strapi-plugin-magic-sessionmanager 4.0.1 → 4.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/README.md +2 -2
- package/admin/src/components/OnlineUsersWidget.jsx +11 -7
- package/admin/src/components/SessionDetailModal.jsx +42 -38
- package/admin/src/components/SessionInfoPanel.jsx +29 -20
- package/admin/src/index.js +9 -0
- package/admin/src/pages/HomePage.jsx +128 -161
- package/admin/src/pages/License.jsx +3 -3
- package/admin/src/pages/Settings.jsx +139 -135
- package/admin/src/pages/UpgradePage.jsx +448 -0
- package/admin/src/pluginId.js +1 -0
- package/admin/src/translations/de.json +294 -15
- package/admin/src/translations/en.json +293 -14
- package/admin/src/translations/es.json +284 -18
- package/admin/src/translations/fr.json +284 -18
- package/admin/src/translations/pt.json +284 -18
- package/admin/src/utils/theme.js +85 -0
- package/dist/_chunks/{Analytics-Dv9f_0eZ.mjs → Analytics-DTE_zmRV.mjs} +2 -2
- package/dist/_chunks/{Analytics-BBdv1I5y.js → Analytics-lw_JaOVy.js} +2 -2
- package/dist/_chunks/{App-CJaZPNjt.js → App-DDKYCjKw.js} +216 -206
- package/dist/_chunks/{App-CIQ-7sa7.mjs → App-DJW1ZNl5.mjs} +216 -206
- package/dist/_chunks/{License-nrmFxoBm.mjs → License-DaOFuImm.mjs} +4 -8
- package/dist/_chunks/{License-D24rgaZQ.js → License-Tk-6UfPl.js} +4 -8
- package/dist/_chunks/{OnlineUsersWidget-B8JS1xZu.js → OnlineUsersWidget-C1qTpsws.js} +11 -7
- package/dist/_chunks/{OnlineUsersWidget-ArMl0nen.mjs → OnlineUsersWidget-CADphbXG.mjs} +11 -7
- package/dist/_chunks/{Settings-D5dLEGc_.mjs → Settings-C9xvckgq.mjs} +191 -179
- package/dist/_chunks/{Settings-CqxgjU0y.js → Settings-DyEAuTNQ.js} +191 -179
- package/dist/_chunks/UpgradePage-Dssk8A0Z.js +354 -0
- package/dist/_chunks/UpgradePage-cINvE9zY.mjs +352 -0
- package/dist/_chunks/de-CDA1V0rF.mjs +292 -0
- package/dist/_chunks/de-I-Q-pWqu.js +292 -0
- package/dist/_chunks/en-Bd7_h-4e.js +292 -0
- package/dist/_chunks/en-DzmOCyzQ.mjs +292 -0
- package/dist/_chunks/es-BcAx18XG.js +277 -0
- package/dist/_chunks/es-Cx-SN6qV.mjs +277 -0
- package/dist/_chunks/fr-DCzYMuJ-.js +277 -0
- package/dist/_chunks/fr-DXlXE5Eo.mjs +277 -0
- package/dist/_chunks/{index-WH04CS1c.js → index-CWcvrfXc.js} +45 -42
- package/dist/_chunks/{index-Duk1_Wrz.mjs → index-DQO9bNP7.mjs} +45 -42
- package/dist/_chunks/pt-21-MAb72.js +277 -0
- package/dist/_chunks/pt-zsdTSjba.mjs +277 -0
- package/dist/_chunks/{useLicense-BwOlCyhc.js → useLicense-DtvJOszr.js} +1 -1
- package/dist/_chunks/{useLicense-Ce8GaxB0.mjs → useLicense-DxbD4Wf8.mjs} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/_chunks/de-BxFx1pwE.js +0 -23
- package/dist/_chunks/de-CdO3s01z.mjs +0 -23
- package/dist/_chunks/en-CsPpPJL3.mjs +0 -23
- package/dist/_chunks/en-RqmpDHdS.js +0 -23
- package/dist/_chunks/es-CuLHazN1.js +0 -23
- package/dist/_chunks/es-Dkmjhy9c.mjs +0 -23
- package/dist/_chunks/fr-BAJp2yhI.js +0 -23
- package/dist/_chunks/fr-Bssg_3UF.mjs +0 -23
- package/dist/_chunks/pt-BAP9cKs3.js +0 -23
- package/dist/_chunks/pt-BVNoNcuY.mjs +0 -23
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useIntl } from 'react-intl';
|
|
2
3
|
import {
|
|
3
4
|
Box,
|
|
4
5
|
Typography,
|
|
@@ -23,6 +24,7 @@ import { Check, Information, Duplicate, Trash, Mail, Code, Cog, Shield, Clock }
|
|
|
23
24
|
import styled, { keyframes, css } from 'styled-components';
|
|
24
25
|
import pluginId from '../pluginId';
|
|
25
26
|
import { useLicense } from '../hooks/useLicense';
|
|
27
|
+
import { getTranslation } from '../utils/getTranslation';
|
|
26
28
|
|
|
27
29
|
// ================ THEME ================
|
|
28
30
|
const theme = {
|
|
@@ -59,9 +61,9 @@ const StickySaveBar = styled(Box)`
|
|
|
59
61
|
position: sticky;
|
|
60
62
|
top: 0;
|
|
61
63
|
z-index: 10;
|
|
62
|
-
background:
|
|
63
|
-
border-bottom: 1px solid ${theme.colors.
|
|
64
|
-
box-shadow:
|
|
64
|
+
background: ${props => props.theme.colors.neutral0};
|
|
65
|
+
border-bottom: 1px solid ${props => props.theme.colors.neutral200};
|
|
66
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
65
67
|
`;
|
|
66
68
|
|
|
67
69
|
const ToggleCard = styled(Box)`
|
|
@@ -380,9 +382,11 @@ const generateSecureKey = () => {
|
|
|
380
382
|
};
|
|
381
383
|
|
|
382
384
|
const SettingsPage = () => {
|
|
385
|
+
const { formatMessage } = useIntl();
|
|
383
386
|
const { get, post, put } = useFetchClient();
|
|
384
387
|
const { toggleNotification } = useNotification();
|
|
385
388
|
const { isPremium, isAdvanced, isEnterprise } = useLicense();
|
|
389
|
+
const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
|
|
386
390
|
const [loading, setLoading] = useState(true);
|
|
387
391
|
const [saving, setSaving] = useState(false);
|
|
388
392
|
const [hasChanges, setHasChanges] = useState(false);
|
|
@@ -459,7 +463,7 @@ const SettingsPage = () => {
|
|
|
459
463
|
console.error('[Settings] Error loading from backend:', err);
|
|
460
464
|
toggleNotification({
|
|
461
465
|
type: 'warning',
|
|
462
|
-
message: 'Could not load settings from server. Using defaults.',
|
|
466
|
+
message: t('notifications.warning.settingsLoad', 'Could not load settings from server. Using defaults.'),
|
|
463
467
|
});
|
|
464
468
|
// Fallback to default settings
|
|
465
469
|
setSettings(prev => ({ ...prev, emailTemplates: getDefaultTemplates() }));
|
|
@@ -486,7 +490,7 @@ const SettingsPage = () => {
|
|
|
486
490
|
if (response?.data?.success) {
|
|
487
491
|
toggleNotification({
|
|
488
492
|
type: 'success',
|
|
489
|
-
message: 'Settings saved successfully to database!',
|
|
493
|
+
message: t('notifications.success.saved', 'Settings saved successfully to database!'),
|
|
490
494
|
});
|
|
491
495
|
|
|
492
496
|
setHasChanges(false);
|
|
@@ -504,7 +508,7 @@ const SettingsPage = () => {
|
|
|
504
508
|
console.error('[Settings] Error saving:', err);
|
|
505
509
|
toggleNotification({
|
|
506
510
|
type: 'danger',
|
|
507
|
-
message: 'Failed to save settings to server',
|
|
511
|
+
message: t('notifications.error.save', 'Failed to save settings to server'),
|
|
508
512
|
});
|
|
509
513
|
} finally {
|
|
510
514
|
setSaving(false);
|
|
@@ -517,7 +521,7 @@ const SettingsPage = () => {
|
|
|
517
521
|
};
|
|
518
522
|
|
|
519
523
|
const handleCleanInactive = async () => {
|
|
520
|
-
if (!confirm('[WARNING] This will permanently delete ALL inactive sessions.\n\nContinue?')) {
|
|
524
|
+
if (!confirm(t('settings.general.danger.confirm', '[WARNING] This will permanently delete ALL inactive sessions.\n\nContinue?'))) {
|
|
521
525
|
return;
|
|
522
526
|
}
|
|
523
527
|
|
|
@@ -527,12 +531,12 @@ const SettingsPage = () => {
|
|
|
527
531
|
|
|
528
532
|
toggleNotification({
|
|
529
533
|
type: 'success',
|
|
530
|
-
message:
|
|
534
|
+
message: t('notifications.success.cleaned', 'Successfully deleted {count} inactive sessions!', { count: data.deletedCount }),
|
|
531
535
|
});
|
|
532
536
|
} catch (err) {
|
|
533
537
|
toggleNotification({
|
|
534
538
|
type: 'danger',
|
|
535
|
-
message: 'Failed to delete inactive sessions',
|
|
539
|
+
message: t('notifications.error.clean', 'Failed to delete inactive sessions'),
|
|
536
540
|
});
|
|
537
541
|
} finally {
|
|
538
542
|
setCleaning(false);
|
|
@@ -542,7 +546,7 @@ const SettingsPage = () => {
|
|
|
542
546
|
if (loading) {
|
|
543
547
|
return (
|
|
544
548
|
<Flex justifyContent="center" padding={8}>
|
|
545
|
-
<Loader>Loading
|
|
549
|
+
<Loader>{t('common.loading', 'Loading...')}</Loader>
|
|
546
550
|
</Flex>
|
|
547
551
|
);
|
|
548
552
|
}
|
|
@@ -554,16 +558,16 @@ const SettingsPage = () => {
|
|
|
554
558
|
<Flex justifyContent="space-between" alignItems="center">
|
|
555
559
|
<Flex direction="column" gap={1} alignItems="flex-start">
|
|
556
560
|
<Typography variant="alpha" fontWeight="bold" style={{ fontSize: '24px' }}>
|
|
557
|
-
⚙️ Session Manager Settings
|
|
561
|
+
⚙️ {t('settings.title', 'Session Manager Settings')}
|
|
558
562
|
</Typography>
|
|
559
563
|
<Typography variant="epsilon" textColor="neutral600">
|
|
560
|
-
Configure session tracking, security, and email notifications
|
|
564
|
+
{t('settings.subtitle', 'Configure session tracking, security, and email notifications')}
|
|
561
565
|
</Typography>
|
|
562
566
|
</Flex>
|
|
563
567
|
<Flex gap={2}>
|
|
564
568
|
{hasChanges && (
|
|
565
569
|
<Button onClick={handleReset} variant="tertiary" size="L">
|
|
566
|
-
Reset
|
|
570
|
+
{t('settings.reset', 'Reset')}
|
|
567
571
|
</Button>
|
|
568
572
|
)}
|
|
569
573
|
<Button
|
|
@@ -594,7 +598,7 @@ const SettingsPage = () => {
|
|
|
594
598
|
e.currentTarget.style.boxShadow = hasChanges && !saving ? '0 4px 12px rgba(102, 126, 234, 0.4)' : 'none';
|
|
595
599
|
}}
|
|
596
600
|
>
|
|
597
|
-
{saving ? 'Saving...' : hasChanges ? 'Save Changes' : 'No Changes'}
|
|
601
|
+
{saving ? t('settings.saving', 'Saving...') : hasChanges ? t('settings.save', 'Save Changes') : t('settings.noChanges', 'No Changes')}
|
|
598
602
|
</Button>
|
|
599
603
|
</Flex>
|
|
600
604
|
</Flex>
|
|
@@ -609,17 +613,17 @@ const SettingsPage = () => {
|
|
|
609
613
|
<Information style={{ width: '20px', height: '20px', color: '#0284C7' }} />
|
|
610
614
|
<Box>
|
|
611
615
|
<Typography variant="omega" fontWeight="bold" textColor="primary700" style={{ marginBottom: '4px' }}>
|
|
612
|
-
Current License Status
|
|
616
|
+
{t('settings.license.title', 'Current License Status')}
|
|
613
617
|
</Typography>
|
|
614
618
|
<Flex gap={3}>
|
|
615
619
|
<Badge backgroundColor={isPremium ? "success100" : "neutral100"} textColor={isPremium ? "success700" : "neutral600"}>
|
|
616
|
-
{isPremium ? '✓' : '✗'} Premium
|
|
620
|
+
{isPremium ? '✓' : '✗'} {t('settings.license.premium', 'Premium')}
|
|
617
621
|
</Badge>
|
|
618
622
|
<Badge backgroundColor={isAdvanced ? "primary100" : "neutral100"} textColor={isAdvanced ? "primary700" : "neutral600"}>
|
|
619
|
-
{isAdvanced ? '✓' : '✗'} Advanced
|
|
623
|
+
{isAdvanced ? '✓' : '✗'} {t('settings.license.advanced', 'Advanced')}
|
|
620
624
|
</Badge>
|
|
621
625
|
<Badge backgroundColor={isEnterprise ? "secondary100" : "neutral100"} textColor={isEnterprise ? "secondary700" : "neutral600"}>
|
|
622
|
-
{isEnterprise ? '✓' : '✗'} Enterprise
|
|
626
|
+
{isEnterprise ? '✓' : '✗'} {t('settings.license.enterprise', 'Enterprise')}
|
|
623
627
|
</Badge>
|
|
624
628
|
</Flex>
|
|
625
629
|
</Box>
|
|
@@ -634,9 +638,9 @@ const SettingsPage = () => {
|
|
|
634
638
|
<Accordion.Header>
|
|
635
639
|
<Accordion.Trigger
|
|
636
640
|
icon={Cog}
|
|
637
|
-
description=
|
|
641
|
+
description={t('settings.general.description', 'Basic session tracking configuration')}
|
|
638
642
|
>
|
|
639
|
-
General Settings
|
|
643
|
+
{t('settings.general.title', 'General Settings')}
|
|
640
644
|
</Accordion.Trigger>
|
|
641
645
|
</Accordion.Header>
|
|
642
646
|
<Accordion.Content>
|
|
@@ -644,27 +648,27 @@ const SettingsPage = () => {
|
|
|
644
648
|
|
|
645
649
|
{/* Session Timeout */}
|
|
646
650
|
<Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
|
|
647
|
-
SESSION TIMEOUT
|
|
651
|
+
{t('settings.general.timeout.title', 'SESSION TIMEOUT')}
|
|
648
652
|
</Typography>
|
|
649
653
|
<Grid.Root gap={6} style={{ marginBottom: '32px' }}>
|
|
650
654
|
<Grid.Item col={6} s={12}>
|
|
651
655
|
<Box>
|
|
652
656
|
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
653
|
-
Inactivity Timeout
|
|
657
|
+
{t('settings.general.timeout.inactivity', 'Inactivity Timeout')}
|
|
654
658
|
</Typography>
|
|
655
659
|
<SingleSelect
|
|
656
660
|
value={String(settings.inactivityTimeout)}
|
|
657
661
|
onChange={(value) => handleChange('inactivityTimeout', parseInt(value))}
|
|
658
662
|
>
|
|
659
|
-
<SingleSelectOption value="5">5 minutes (Very Strict)</SingleSelectOption>
|
|
660
|
-
<SingleSelectOption value="10">10 minutes (Strict)</SingleSelectOption>
|
|
661
|
-
<SingleSelectOption value="15">15 minutes (Recommended)</SingleSelectOption>
|
|
662
|
-
<SingleSelectOption value="30">30 minutes (Moderate)</SingleSelectOption>
|
|
663
|
-
<SingleSelectOption value="60">1 hour (Relaxed)</SingleSelectOption>
|
|
664
|
-
<SingleSelectOption value="120">2 hours (Very Relaxed)</SingleSelectOption>
|
|
663
|
+
<SingleSelectOption value="5">{t('settings.general.timeout.5min', '5 minutes (Very Strict)')}</SingleSelectOption>
|
|
664
|
+
<SingleSelectOption value="10">{t('settings.general.timeout.10min', '10 minutes (Strict)')}</SingleSelectOption>
|
|
665
|
+
<SingleSelectOption value="15">{t('settings.general.timeout.15min', '15 minutes (Recommended)')}</SingleSelectOption>
|
|
666
|
+
<SingleSelectOption value="30">{t('settings.general.timeout.30min', '30 minutes (Moderate)')}</SingleSelectOption>
|
|
667
|
+
<SingleSelectOption value="60">{t('settings.general.timeout.1hour', '1 hour (Relaxed)')}</SingleSelectOption>
|
|
668
|
+
<SingleSelectOption value="120">{t('settings.general.timeout.2hours', '2 hours (Very Relaxed)')}</SingleSelectOption>
|
|
665
669
|
</SingleSelect>
|
|
666
670
|
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
|
|
667
|
-
Sessions inactive for more than {
|
|
671
|
+
{t('settings.general.timeout.inactivityHint', 'Sessions inactive for more than {minutes} minutes will be marked as offline', { minutes: settings.inactivityTimeout })}
|
|
668
672
|
</Typography>
|
|
669
673
|
</Box>
|
|
670
674
|
</Grid.Item>
|
|
@@ -672,20 +676,20 @@ const SettingsPage = () => {
|
|
|
672
676
|
<Grid.Item col={6} s={12}>
|
|
673
677
|
<Box>
|
|
674
678
|
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
675
|
-
Last Seen Rate Limit
|
|
679
|
+
{t('settings.general.rateLimit.title', 'Last Seen Rate Limit')}
|
|
676
680
|
</Typography>
|
|
677
681
|
<SingleSelect
|
|
678
682
|
value={String(settings.lastSeenRateLimit)}
|
|
679
683
|
onChange={(value) => handleChange('lastSeenRateLimit', parseInt(value))}
|
|
680
684
|
>
|
|
681
|
-
<SingleSelectOption value="10">10 seconds</SingleSelectOption>
|
|
682
|
-
<SingleSelectOption value="30">30 seconds (Recommended)</SingleSelectOption>
|
|
683
|
-
<SingleSelectOption value="60">1 minute</SingleSelectOption>
|
|
684
|
-
<SingleSelectOption value="120">2 minutes</SingleSelectOption>
|
|
685
|
-
<SingleSelectOption value="300">5 minutes</SingleSelectOption>
|
|
685
|
+
<SingleSelectOption value="10">{t('settings.general.rateLimit.10sec', '10 seconds')}</SingleSelectOption>
|
|
686
|
+
<SingleSelectOption value="30">{t('settings.general.rateLimit.30sec', '30 seconds (Recommended)')}</SingleSelectOption>
|
|
687
|
+
<SingleSelectOption value="60">{t('settings.general.rateLimit.1min', '1 minute')}</SingleSelectOption>
|
|
688
|
+
<SingleSelectOption value="120">{t('settings.general.rateLimit.2min', '2 minutes')}</SingleSelectOption>
|
|
689
|
+
<SingleSelectOption value="300">{t('settings.general.rateLimit.5min', '5 minutes')}</SingleSelectOption>
|
|
686
690
|
</SingleSelect>
|
|
687
691
|
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
|
|
688
|
-
Prevents excessive database writes. Updates throttled to once every {settings.lastSeenRateLimit}
|
|
692
|
+
{t('settings.general.rateLimit.hint', 'Prevents excessive database writes. Updates throttled to once every {seconds} seconds', { seconds: settings.lastSeenRateLimit })}
|
|
689
693
|
</Typography>
|
|
690
694
|
</Box>
|
|
691
695
|
</Grid.Item>
|
|
@@ -694,25 +698,25 @@ const SettingsPage = () => {
|
|
|
694
698
|
{/* Cleanup & Retention */}
|
|
695
699
|
<Divider style={{ marginBottom: '24px' }} />
|
|
696
700
|
<Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
|
|
697
|
-
🧹 AUTO-CLEANUP & RETENTION
|
|
701
|
+
🧹 {t('settings.general.cleanup.title', 'AUTO-CLEANUP & RETENTION')}
|
|
698
702
|
</Typography>
|
|
699
703
|
<Grid.Root gap={6}>
|
|
700
704
|
<Grid.Item col={6} s={12}>
|
|
701
705
|
<Box>
|
|
702
706
|
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
703
|
-
Cleanup Interval
|
|
707
|
+
{t('settings.general.cleanup.interval', 'Cleanup Interval')}
|
|
704
708
|
</Typography>
|
|
705
709
|
<SingleSelect
|
|
706
710
|
value={String(settings.cleanupInterval)}
|
|
707
711
|
onChange={(value) => handleChange('cleanupInterval', parseInt(value))}
|
|
708
712
|
>
|
|
709
|
-
<SingleSelectOption value="15">15 minutes</SingleSelectOption>
|
|
710
|
-
<SingleSelectOption value="30">30 minutes (Recommended)</SingleSelectOption>
|
|
711
|
-
<SingleSelectOption value="60">1 hour</SingleSelectOption>
|
|
712
|
-
<SingleSelectOption value="120">2 hours</SingleSelectOption>
|
|
713
|
+
<SingleSelectOption value="15">{t('settings.general.cleanup.15min', '15 minutes')}</SingleSelectOption>
|
|
714
|
+
<SingleSelectOption value="30">{t('settings.general.cleanup.30min', '30 minutes (Recommended)')}</SingleSelectOption>
|
|
715
|
+
<SingleSelectOption value="60">{t('settings.general.cleanup.1hour', '1 hour')}</SingleSelectOption>
|
|
716
|
+
<SingleSelectOption value="120">{t('settings.general.cleanup.2hours', '2 hours')}</SingleSelectOption>
|
|
713
717
|
</SingleSelect>
|
|
714
718
|
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
|
|
715
|
-
Inactive sessions are automatically cleaned every {settings.cleanupInterval}
|
|
719
|
+
{t('settings.general.cleanup.intervalHint', 'Inactive sessions are automatically cleaned every {minutes} minutes', { minutes: settings.cleanupInterval })}
|
|
716
720
|
</Typography>
|
|
717
721
|
</Box>
|
|
718
722
|
</Grid.Item>
|
|
@@ -720,22 +724,25 @@ const SettingsPage = () => {
|
|
|
720
724
|
<Grid.Item col={6} s={12}>
|
|
721
725
|
<Box>
|
|
722
726
|
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
723
|
-
Retention Period
|
|
727
|
+
{t('settings.general.retention.title', 'Retention Period')}
|
|
724
728
|
</Typography>
|
|
725
729
|
<SingleSelect
|
|
726
730
|
value={String(settings.retentionDays)}
|
|
727
731
|
onChange={(value) => handleChange('retentionDays', parseInt(value))}
|
|
728
732
|
>
|
|
729
|
-
<SingleSelectOption value="7">7 days</SingleSelectOption>
|
|
730
|
-
<SingleSelectOption value="30">30 days</SingleSelectOption>
|
|
731
|
-
<SingleSelectOption value="60">60 days</SingleSelectOption>
|
|
732
|
-
<SingleSelectOption value="90">90 days (Recommended)</SingleSelectOption>
|
|
733
|
-
<SingleSelectOption value="180">180 days</SingleSelectOption>
|
|
734
|
-
<SingleSelectOption value="365">1 year</SingleSelectOption>
|
|
735
|
-
<SingleSelectOption value="-1">Forever</SingleSelectOption>
|
|
733
|
+
<SingleSelectOption value="7">{t('settings.general.retention.7days', '7 days')}</SingleSelectOption>
|
|
734
|
+
<SingleSelectOption value="30">{t('settings.general.retention.30days', '30 days')}</SingleSelectOption>
|
|
735
|
+
<SingleSelectOption value="60">{t('settings.general.retention.60days', '60 days')}</SingleSelectOption>
|
|
736
|
+
<SingleSelectOption value="90">{t('settings.general.retention.90days', '90 days (Recommended)')}</SingleSelectOption>
|
|
737
|
+
<SingleSelectOption value="180">{t('settings.general.retention.180days', '180 days')}</SingleSelectOption>
|
|
738
|
+
<SingleSelectOption value="365">{t('settings.general.retention.1year', '1 year')}</SingleSelectOption>
|
|
739
|
+
<SingleSelectOption value="-1">{t('settings.general.retention.forever', 'Forever')}</SingleSelectOption>
|
|
736
740
|
</SingleSelect>
|
|
737
741
|
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '8px' }}>
|
|
738
|
-
|
|
742
|
+
{settings.retentionDays === -1
|
|
743
|
+
? t('settings.general.retention.hintNever', 'Old sessions deleted after never')
|
|
744
|
+
: t('settings.general.retention.hint', 'Old sessions deleted after {days}', { days: `${settings.retentionDays} days` })
|
|
745
|
+
}
|
|
739
746
|
</Typography>
|
|
740
747
|
</Box>
|
|
741
748
|
</Grid.Item>
|
|
@@ -746,10 +753,10 @@ const SettingsPage = () => {
|
|
|
746
753
|
<Trash style={{ width: '18px', height: '18px', color: theme.colors.danger[600], flexShrink: 0, marginTop: '2px' }} />
|
|
747
754
|
<Box style={{ flex: 1 }}>
|
|
748
755
|
<Typography variant="omega" fontWeight="bold" textColor="danger700" style={{ marginBottom: '8px', display: 'block' }}>
|
|
749
|
-
Danger Zone
|
|
756
|
+
{t('settings.general.danger.title', 'Danger Zone')}
|
|
750
757
|
</Typography>
|
|
751
758
|
<Typography variant="pi" textColor="danger600" style={{ fontSize: '13px', lineHeight: '1.7' }}>
|
|
752
|
-
|
|
759
|
+
{t('settings.general.danger.description', 'Clean All Inactive: Permanently deletes all inactive sessions. This cannot be undone.')}
|
|
753
760
|
</Typography>
|
|
754
761
|
</Box>
|
|
755
762
|
<Button
|
|
@@ -760,7 +767,7 @@ const SettingsPage = () => {
|
|
|
760
767
|
size="S"
|
|
761
768
|
style={{ flexShrink: 0 }}
|
|
762
769
|
>
|
|
763
|
-
Clean Now
|
|
770
|
+
{t('settings.general.danger.cleanNow', 'Clean Now')}
|
|
764
771
|
</Button>
|
|
765
772
|
</Flex>
|
|
766
773
|
</Box>
|
|
@@ -776,16 +783,16 @@ const SettingsPage = () => {
|
|
|
776
783
|
<Accordion.Header>
|
|
777
784
|
<Accordion.Trigger
|
|
778
785
|
icon={Shield}
|
|
779
|
-
description=
|
|
786
|
+
description={t('settings.security.description', 'Security policies and threat protection')}
|
|
780
787
|
>
|
|
781
|
-
Security Settings
|
|
788
|
+
{t('settings.security.title', 'Security Settings')}
|
|
782
789
|
</Accordion.Trigger>
|
|
783
790
|
</Accordion.Header>
|
|
784
791
|
<Accordion.Content>
|
|
785
792
|
<Box padding={6}>
|
|
786
793
|
|
|
787
794
|
<Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
|
|
788
|
-
SECURITY OPTIONS
|
|
795
|
+
{t('settings.security.options', 'SECURITY OPTIONS')}
|
|
789
796
|
</Typography>
|
|
790
797
|
|
|
791
798
|
{/* Encryption Key Generator */}
|
|
@@ -803,30 +810,29 @@ const SettingsPage = () => {
|
|
|
803
810
|
<Flex alignItems="center" gap={3}>
|
|
804
811
|
<Shield style={{ width: 24, height: 24, color: theme.colors.primary[600] }} />
|
|
805
812
|
<Typography variant="delta" fontWeight="bold">
|
|
806
|
-
JWT Encryption Key Generator
|
|
813
|
+
{t('settings.security.encryption.title', 'JWT Encryption Key Generator')}
|
|
807
814
|
</Typography>
|
|
808
815
|
</Flex>
|
|
809
816
|
|
|
810
817
|
<Typography variant="omega" textColor="neutral600" style={{ lineHeight: 1.6 }}>
|
|
811
|
-
Generate a secure 32-character encryption key for JWT token storage.
|
|
812
|
-
This key is used to encrypt tokens before saving them to the database.
|
|
818
|
+
{t('settings.security.encryption.description', 'Generate a secure 32-character encryption key for JWT token storage. This key is used to encrypt tokens before saving them to the database.')}
|
|
813
819
|
</Typography>
|
|
814
820
|
|
|
815
821
|
<Alert
|
|
816
822
|
variant="default"
|
|
817
|
-
title=
|
|
823
|
+
title={t('settings.security.encryption.important', 'Important')}
|
|
818
824
|
style={{ marginTop: 8 }}
|
|
819
825
|
>
|
|
820
|
-
Add this key to your
|
|
826
|
+
{t('settings.security.encryption.envHint', 'Add this key to your .env file as SESSION_ENCRYPTION_KEY for production.')}
|
|
821
827
|
</Alert>
|
|
822
828
|
|
|
823
829
|
<Flex gap={3} alignItems="flex-end">
|
|
824
830
|
<Box style={{ flex: 1 }}>
|
|
825
831
|
<TextInput
|
|
826
|
-
label=
|
|
832
|
+
label={t('settings.security.encryption.label', 'Generated Encryption Key')}
|
|
827
833
|
value={encryptionKey}
|
|
828
834
|
onChange={(e) => setEncryptionKey(e.target.value)}
|
|
829
|
-
placeholder="Click 'Generate Key' to create a secure key"
|
|
835
|
+
placeholder={t('settings.security.encryption.placeholder', "Click 'Generate Key' to create a secure key")}
|
|
830
836
|
type={showEncryptionKey ? 'text' : 'password'}
|
|
831
837
|
/>
|
|
832
838
|
</Box>
|
|
@@ -835,7 +841,7 @@ const SettingsPage = () => {
|
|
|
835
841
|
onClick={() => setShowEncryptionKey(!showEncryptionKey)}
|
|
836
842
|
size="L"
|
|
837
843
|
>
|
|
838
|
-
{showEncryptionKey ? 'Hide' : 'Show'}
|
|
844
|
+
{showEncryptionKey ? t('settings.security.encryption.hide', 'Hide') : t('settings.security.encryption.show', 'Show')}
|
|
839
845
|
</Button>
|
|
840
846
|
</Flex>
|
|
841
847
|
|
|
@@ -849,12 +855,12 @@ const SettingsPage = () => {
|
|
|
849
855
|
setShowEncryptionKey(true);
|
|
850
856
|
toggleNotification({
|
|
851
857
|
type: 'success',
|
|
852
|
-
message: '32-character encryption key generated!'
|
|
858
|
+
message: t('notifications.success.keyGenerated', '32-character encryption key generated!')
|
|
853
859
|
});
|
|
854
860
|
}}
|
|
855
861
|
size="L"
|
|
856
862
|
>
|
|
857
|
-
Generate Key
|
|
863
|
+
{t('settings.security.encryption.generate', 'Generate Key')}
|
|
858
864
|
</Button>
|
|
859
865
|
|
|
860
866
|
<Button
|
|
@@ -865,14 +871,14 @@ const SettingsPage = () => {
|
|
|
865
871
|
navigator.clipboard.writeText(encryptionKey);
|
|
866
872
|
toggleNotification({
|
|
867
873
|
type: 'success',
|
|
868
|
-
message: 'Encryption key copied to clipboard!'
|
|
874
|
+
message: t('notifications.success.keyCopied', 'Encryption key copied to clipboard!')
|
|
869
875
|
});
|
|
870
876
|
}
|
|
871
877
|
}}
|
|
872
878
|
disabled={!encryptionKey}
|
|
873
879
|
size="L"
|
|
874
880
|
>
|
|
875
|
-
Copy to Clipboard
|
|
881
|
+
{t('settings.security.encryption.copy', 'Copy to Clipboard')}
|
|
876
882
|
</Button>
|
|
877
883
|
|
|
878
884
|
<Button
|
|
@@ -884,14 +890,14 @@ const SettingsPage = () => {
|
|
|
884
890
|
navigator.clipboard.writeText(envLine);
|
|
885
891
|
toggleNotification({
|
|
886
892
|
type: 'success',
|
|
887
|
-
message: 'Copied as .env format!'
|
|
893
|
+
message: t('notifications.success.envCopied', 'Copied as .env format!')
|
|
888
894
|
});
|
|
889
895
|
}
|
|
890
896
|
}}
|
|
891
897
|
disabled={!encryptionKey}
|
|
892
898
|
size="L"
|
|
893
899
|
>
|
|
894
|
-
Copy for .env
|
|
900
|
+
{t('settings.security.encryption.copyEnv', 'Copy for .env')}
|
|
895
901
|
</Button>
|
|
896
902
|
</Flex>
|
|
897
903
|
|
|
@@ -908,7 +914,7 @@ const SettingsPage = () => {
|
|
|
908
914
|
}}
|
|
909
915
|
>
|
|
910
916
|
<Typography variant="omega" fontWeight="bold" style={{ marginBottom: 8, display: 'block' }}>
|
|
911
|
-
Add to .env file:
|
|
917
|
+
{t('settings.security.encryption.envLabel', 'Add to .env file:')}
|
|
912
918
|
</Typography>
|
|
913
919
|
<code style={{ color: theme.colors.primary[700] }}>
|
|
914
920
|
SESSION_ENCRYPTION_KEY={encryptionKey}
|
|
@@ -940,10 +946,10 @@ const SettingsPage = () => {
|
|
|
940
946
|
textColor={settings.blockSuspiciousSessions ? 'success700' : 'neutral800'}
|
|
941
947
|
style={{ fontSize: '16px' }}
|
|
942
948
|
>
|
|
943
|
-
Block Suspicious Sessions
|
|
949
|
+
{t('settings.security.blockSuspicious.title', 'Block Suspicious Sessions')}
|
|
944
950
|
</Typography>
|
|
945
951
|
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
|
|
946
|
-
Automatically block sessions from VPNs, proxies, or threat IPs
|
|
952
|
+
{t('settings.security.blockSuspicious.description', 'Automatically block sessions from VPNs, proxies, or threat IPs')}
|
|
947
953
|
</Typography>
|
|
948
954
|
</Flex>
|
|
949
955
|
</Flex>
|
|
@@ -971,10 +977,10 @@ const SettingsPage = () => {
|
|
|
971
977
|
textColor={settings.enableGeolocation ? 'success700' : 'neutral800'}
|
|
972
978
|
style={{ fontSize: '16px' }}
|
|
973
979
|
>
|
|
974
|
-
IP Geolocation
|
|
980
|
+
{t('settings.security.geolocation.title', 'IP Geolocation')}
|
|
975
981
|
</Typography>
|
|
976
982
|
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
|
|
977
|
-
Fetch location data for each session (Premium)
|
|
983
|
+
{t('settings.security.geolocation.description', 'Fetch location data for each session (Premium)')}
|
|
978
984
|
</Typography>
|
|
979
985
|
</Flex>
|
|
980
986
|
</Flex>
|
|
@@ -1000,10 +1006,10 @@ const SettingsPage = () => {
|
|
|
1000
1006
|
textColor={settings.enableSecurityScoring ? 'success700' : 'neutral800'}
|
|
1001
1007
|
style={{ fontSize: '16px' }}
|
|
1002
1008
|
>
|
|
1003
|
-
Security Scoring
|
|
1009
|
+
{t('settings.security.scoring.title', 'Security Scoring')}
|
|
1004
1010
|
</Typography>
|
|
1005
1011
|
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
|
|
1006
|
-
Calculate security scores and detect threats (Premium)
|
|
1012
|
+
{t('settings.security.scoring.description', 'Calculate security scores and detect threats (Premium)')}
|
|
1007
1013
|
</Typography>
|
|
1008
1014
|
</Flex>
|
|
1009
1015
|
</Flex>
|
|
@@ -1019,7 +1025,7 @@ const SettingsPage = () => {
|
|
|
1019
1025
|
<Grid.Item col={6} s={12}>
|
|
1020
1026
|
<Box>
|
|
1021
1027
|
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
1022
|
-
🚫 Max Failed Login Attempts
|
|
1028
|
+
🚫 {t('settings.security.maxFailed.title', 'Max Failed Login Attempts')}
|
|
1023
1029
|
</Typography>
|
|
1024
1030
|
<NumberInput
|
|
1025
1031
|
value={settings.maxFailedLogins}
|
|
@@ -1029,7 +1035,7 @@ const SettingsPage = () => {
|
|
|
1029
1035
|
/>
|
|
1030
1036
|
<Box padding={2} background="warning50" style={{ borderRadius: '4px', marginTop: '8px' }}>
|
|
1031
1037
|
<Typography variant="pi" textColor="warning700" style={{ fontSize: '11px' }}>
|
|
1032
|
-
User will be blocked after {
|
|
1038
|
+
{t('settings.security.maxFailed.hint', 'User will be blocked after {count} failed attempts', { count: settings.maxFailedLogins })}
|
|
1033
1039
|
</Typography>
|
|
1034
1040
|
</Box>
|
|
1035
1041
|
</Box>
|
|
@@ -1046,9 +1052,9 @@ const SettingsPage = () => {
|
|
|
1046
1052
|
<Accordion.Header>
|
|
1047
1053
|
<Accordion.Trigger
|
|
1048
1054
|
icon={Mail}
|
|
1049
|
-
description=
|
|
1055
|
+
description={t('settings.email.description', 'Email alerts for security events')}
|
|
1050
1056
|
>
|
|
1051
|
-
Email Notifications (Advanced)
|
|
1057
|
+
{t('settings.email.title', 'Email Notifications (Advanced)')}
|
|
1052
1058
|
</Accordion.Trigger>
|
|
1053
1059
|
</Accordion.Header>
|
|
1054
1060
|
<Accordion.Content>
|
|
@@ -1057,10 +1063,10 @@ const SettingsPage = () => {
|
|
|
1057
1063
|
{/* Email Alerts Toggle */}
|
|
1058
1064
|
<Box background="neutral100" padding={5} style={{ borderRadius: theme.borderRadius.md, marginBottom: '32px' }}>
|
|
1059
1065
|
<Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '8px', display: 'block', textAlign: 'center', color: theme.colors.neutral[700] }}>
|
|
1060
|
-
📧 EMAIL ALERTS
|
|
1066
|
+
📧 {t('settings.email.alerts.title', 'EMAIL ALERTS')}
|
|
1061
1067
|
</Typography>
|
|
1062
1068
|
<Typography variant="pi" textColor="neutral600" style={{ marginBottom: '20px', display: 'block', textAlign: 'center', fontSize: '12px' }}>
|
|
1063
|
-
Send security alerts to users via email
|
|
1069
|
+
{t('settings.email.alerts.subtitle', 'Send security alerts to users via email')}
|
|
1064
1070
|
</Typography>
|
|
1065
1071
|
<Grid.Root gap={4}>
|
|
1066
1072
|
<Grid.Item col={12}>
|
|
@@ -1082,10 +1088,10 @@ const SettingsPage = () => {
|
|
|
1082
1088
|
textColor={settings.enableEmailAlerts ? 'success700' : 'neutral800'}
|
|
1083
1089
|
style={{ fontSize: '16px' }}
|
|
1084
1090
|
>
|
|
1085
|
-
Enable Email Alerts
|
|
1091
|
+
{t('settings.email.enable.title', 'Enable Email Alerts')}
|
|
1086
1092
|
</Typography>
|
|
1087
1093
|
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
|
|
1088
|
-
Send security alerts for suspicious logins, new locations, and VPN/Proxy usage
|
|
1094
|
+
{t('settings.email.enable.description', 'Send security alerts for suspicious logins, new locations, and VPN/Proxy usage')}
|
|
1089
1095
|
</Typography>
|
|
1090
1096
|
</Flex>
|
|
1091
1097
|
</Flex>
|
|
@@ -1098,7 +1104,7 @@ const SettingsPage = () => {
|
|
|
1098
1104
|
{settings.enableEmailAlerts && (
|
|
1099
1105
|
<>
|
|
1100
1106
|
<Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
|
|
1101
|
-
⚙️ ALERT TYPES
|
|
1107
|
+
⚙️ {t('settings.email.types.title', 'ALERT TYPES')}
|
|
1102
1108
|
</Typography>
|
|
1103
1109
|
<Grid.Root gap={4} style={{ marginBottom: '32px' }}>
|
|
1104
1110
|
<Grid.Item col={4} s={12}>
|
|
@@ -1118,7 +1124,7 @@ const SettingsPage = () => {
|
|
|
1118
1124
|
onChange={() => handleChange('alertOnSuspiciousLogin', !settings.alertOnSuspiciousLogin)}
|
|
1119
1125
|
>
|
|
1120
1126
|
<Typography variant="omega" fontWeight="semiBold" style={{ fontSize: '14px' }}>
|
|
1121
|
-
Suspicious Login
|
|
1127
|
+
{t('settings.email.types.suspicious', 'Suspicious Login')}
|
|
1122
1128
|
</Typography>
|
|
1123
1129
|
</Checkbox>
|
|
1124
1130
|
</Box>
|
|
@@ -1140,7 +1146,7 @@ const SettingsPage = () => {
|
|
|
1140
1146
|
onChange={() => handleChange('alertOnNewLocation', !settings.alertOnNewLocation)}
|
|
1141
1147
|
>
|
|
1142
1148
|
<Typography variant="omega" fontWeight="semiBold" style={{ fontSize: '14px' }}>
|
|
1143
|
-
New Location
|
|
1149
|
+
{t('settings.email.types.newLocation', 'New Location')}
|
|
1144
1150
|
</Typography>
|
|
1145
1151
|
</Checkbox>
|
|
1146
1152
|
</Box>
|
|
@@ -1162,7 +1168,7 @@ const SettingsPage = () => {
|
|
|
1162
1168
|
onChange={() => handleChange('alertOnVpnProxy', !settings.alertOnVpnProxy)}
|
|
1163
1169
|
>
|
|
1164
1170
|
<Typography variant="omega" fontWeight="semiBold" style={{ fontSize: '14px' }}>
|
|
1165
|
-
VPN/Proxy
|
|
1171
|
+
{t('settings.email.types.vpnProxy', 'VPN/Proxy')}
|
|
1166
1172
|
</Typography>
|
|
1167
1173
|
</Checkbox>
|
|
1168
1174
|
</Box>
|
|
@@ -1172,18 +1178,18 @@ const SettingsPage = () => {
|
|
|
1172
1178
|
{/* Email Templates */}
|
|
1173
1179
|
<Divider style={{ marginBottom: '24px' }} />
|
|
1174
1180
|
<Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '8px', display: 'block', color: theme.colors.neutral[700] }}>
|
|
1175
|
-
EMAIL TEMPLATES
|
|
1181
|
+
{t('settings.email.templates.title', 'EMAIL TEMPLATES')}
|
|
1176
1182
|
</Typography>
|
|
1177
1183
|
<Typography variant="pi" textColor="neutral600" style={{ marginBottom: '20px', display: 'block', fontSize: '12px' }}>
|
|
1178
|
-
Customize email notification templates with dynamic variables
|
|
1184
|
+
{t('settings.email.templates.subtitle', 'Customize email notification templates with dynamic variables')}
|
|
1179
1185
|
</Typography>
|
|
1180
1186
|
|
|
1181
1187
|
{/* Template Tabs */}
|
|
1182
1188
|
<Tabs.Root value={activeTemplateTab} onValueChange={setActiveTemplateTab}>
|
|
1183
1189
|
<Tabs.List aria-label="Email Templates">
|
|
1184
|
-
<Tabs.Trigger value="suspiciousLogin">Suspicious Login</Tabs.Trigger>
|
|
1185
|
-
<Tabs.Trigger value="newLocation">New Location</Tabs.Trigger>
|
|
1186
|
-
<Tabs.Trigger value="vpnProxy">VPN/Proxy</Tabs.Trigger>
|
|
1190
|
+
<Tabs.Trigger value="suspiciousLogin">{t('settings.email.templates.tab.suspicious', 'Suspicious Login')}</Tabs.Trigger>
|
|
1191
|
+
<Tabs.Trigger value="newLocation">{t('settings.email.templates.tab.newLocation', 'New Location')}</Tabs.Trigger>
|
|
1192
|
+
<Tabs.Trigger value="vpnProxy">{t('settings.email.templates.tab.vpnProxy', 'VPN/Proxy')}</Tabs.Trigger>
|
|
1187
1193
|
</Tabs.List>
|
|
1188
1194
|
|
|
1189
1195
|
{Object.keys(settings.emailTemplates).map((templateKey) => (
|
|
@@ -1192,7 +1198,7 @@ const SettingsPage = () => {
|
|
|
1192
1198
|
{/* Subject */}
|
|
1193
1199
|
<Box style={{ marginBottom: '24px' }}>
|
|
1194
1200
|
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
|
|
1195
|
-
Email Subject
|
|
1201
|
+
{t('settings.email.templates.subject', 'Email Subject')}
|
|
1196
1202
|
</Typography>
|
|
1197
1203
|
<TextInput
|
|
1198
1204
|
value={settings.emailTemplates[templateKey].subject}
|
|
@@ -1201,7 +1207,7 @@ const SettingsPage = () => {
|
|
|
1201
1207
|
newTemplates[templateKey].subject = e.target.value;
|
|
1202
1208
|
handleChange('emailTemplates', newTemplates);
|
|
1203
1209
|
}}
|
|
1204
|
-
placeholder=
|
|
1210
|
+
placeholder={t('settings.email.templates.subjectPlaceholder', 'Enter email subject...')}
|
|
1205
1211
|
/>
|
|
1206
1212
|
</Box>
|
|
1207
1213
|
|
|
@@ -1215,7 +1221,7 @@ const SettingsPage = () => {
|
|
|
1215
1221
|
<Flex alignItems="center" gap={2}>
|
|
1216
1222
|
<Code style={{ width: '16px', height: '16px', color: theme.colors.primary[600] }} />
|
|
1217
1223
|
<Typography variant="omega" fontWeight="bold" textColor="primary600">
|
|
1218
|
-
Available Variables (click to copy)
|
|
1224
|
+
{t('settings.email.templates.variables', 'Available Variables (click to copy)')}
|
|
1219
1225
|
</Typography>
|
|
1220
1226
|
</Flex>
|
|
1221
1227
|
<Flex gap={2} wrap="wrap">
|
|
@@ -1226,7 +1232,7 @@ const SettingsPage = () => {
|
|
|
1226
1232
|
variant="tertiary"
|
|
1227
1233
|
onClick={() => {
|
|
1228
1234
|
navigator.clipboard.writeText(variable);
|
|
1229
|
-
toggleNotification({ type: 'success', message:
|
|
1235
|
+
toggleNotification({ type: 'success', message: t('notifications.success.variableCopied', '{variable} copied!', { variable }) });
|
|
1230
1236
|
}}
|
|
1231
1237
|
style={{
|
|
1232
1238
|
fontFamily: 'monospace',
|
|
@@ -1251,9 +1257,9 @@ const SettingsPage = () => {
|
|
|
1251
1257
|
<Flex justifyContent="space-between" alignItems="center" style={{ marginBottom: '16px' }}>
|
|
1252
1258
|
<Flex alignItems="center" gap={2}>
|
|
1253
1259
|
<Typography variant="delta" fontWeight="bold" style={{ fontSize: '18px' }}>
|
|
1254
|
-
🎨 HTML Template
|
|
1260
|
+
🎨 {t('settings.email.templates.html.title', 'HTML Template')}
|
|
1255
1261
|
</Typography>
|
|
1256
|
-
<Badge variant="success">Main Template</Badge>
|
|
1262
|
+
<Badge variant="success">{t('settings.email.templates.html.badge', 'Main Template')}</Badge>
|
|
1257
1263
|
</Flex>
|
|
1258
1264
|
<Button
|
|
1259
1265
|
variant="tertiary"
|
|
@@ -1263,14 +1269,14 @@ const SettingsPage = () => {
|
|
|
1263
1269
|
const newTemplates = { ...settings.emailTemplates };
|
|
1264
1270
|
newTemplates[templateKey].html = defaultTemplates[templateKey].html;
|
|
1265
1271
|
handleChange('emailTemplates', newTemplates);
|
|
1266
|
-
toggleNotification({ type: 'success', message: 'Default
|
|
1272
|
+
toggleNotification({ type: 'success', message: t('notifications.success.defaultLoaded', 'Default template loaded!') });
|
|
1267
1273
|
}}
|
|
1268
1274
|
>
|
|
1269
|
-
📋 Load Default
|
|
1275
|
+
📋 {t('settings.email.templates.html.loadDefault', 'Load Default')}
|
|
1270
1276
|
</Button>
|
|
1271
1277
|
</Flex>
|
|
1272
1278
|
<Typography variant="pi" textColor="neutral600" style={{ marginBottom: '16px', display: 'block', fontSize: '14px' }}>
|
|
1273
|
-
HTML template for email notifications. Use variables like
|
|
1279
|
+
{t('settings.email.templates.html.description', 'HTML template for email notifications. Use variables like {{user.email}} for dynamic content.')}
|
|
1274
1280
|
</Typography>
|
|
1275
1281
|
<Box
|
|
1276
1282
|
style={{
|
|
@@ -1325,10 +1331,10 @@ const SettingsPage = () => {
|
|
|
1325
1331
|
size="S"
|
|
1326
1332
|
onClick={() => {
|
|
1327
1333
|
navigator.clipboard.writeText(settings.emailTemplates[templateKey].html);
|
|
1328
|
-
toggleNotification({ type: 'success', message: 'HTML template copied!' });
|
|
1334
|
+
toggleNotification({ type: 'success', message: t('notifications.success.htmlCopied', 'HTML template copied!') });
|
|
1329
1335
|
}}
|
|
1330
1336
|
>
|
|
1331
|
-
📋 Copy Template
|
|
1337
|
+
📋 {t('settings.email.templates.html.copy', 'Copy Template')}
|
|
1332
1338
|
</Button>
|
|
1333
1339
|
<Button
|
|
1334
1340
|
variant="tertiary"
|
|
@@ -1338,12 +1344,12 @@ const SettingsPage = () => {
|
|
|
1338
1344
|
toggleNotification({
|
|
1339
1345
|
type: validation.isValid ? 'success' : 'warning',
|
|
1340
1346
|
message: validation.isValid
|
|
1341
|
-
?
|
|
1342
|
-
: '[WARNING] No variables found. Add at least one variable.',
|
|
1347
|
+
? t('notifications.success.validated', 'Template valid! Found {found}/{total} variables.', { found: validation.foundVars.length, total: validation.totalAvailable })
|
|
1348
|
+
: t('notifications.warning.noVariables', '[WARNING] No variables found. Add at least one variable.'),
|
|
1343
1349
|
});
|
|
1344
1350
|
}}
|
|
1345
1351
|
>
|
|
1346
|
-
✓ Validate
|
|
1352
|
+
✓ {t('settings.email.templates.html.validate', 'Validate')}
|
|
1347
1353
|
</Button>
|
|
1348
1354
|
<Button
|
|
1349
1355
|
variant="tertiary"
|
|
@@ -1353,11 +1359,11 @@ const SettingsPage = () => {
|
|
|
1353
1359
|
const chars = settings.emailTemplates[templateKey].html.length;
|
|
1354
1360
|
toggleNotification({
|
|
1355
1361
|
type: 'info',
|
|
1356
|
-
message:
|
|
1362
|
+
message: t('notifications.info.templateStats', 'Template has {lines} lines and {chars} characters', { lines, chars })
|
|
1357
1363
|
});
|
|
1358
1364
|
}}
|
|
1359
1365
|
>
|
|
1360
|
-
ℹ️ Template Info
|
|
1366
|
+
ℹ️ {t('settings.email.templates.html.info', 'Template Info')}
|
|
1361
1367
|
</Button>
|
|
1362
1368
|
</Flex>
|
|
1363
1369
|
</Box>
|
|
@@ -1371,9 +1377,9 @@ const SettingsPage = () => {
|
|
|
1371
1377
|
<Flex justifyContent="space-between" alignItems="center" style={{ marginBottom: '16px' }}>
|
|
1372
1378
|
<Flex alignItems="center" gap={2}>
|
|
1373
1379
|
<Typography variant="delta" fontWeight="bold" style={{ fontSize: '18px' }}>
|
|
1374
|
-
📄 Text Template
|
|
1380
|
+
📄 {t('settings.email.templates.text.title', 'Text Template')}
|
|
1375
1381
|
</Typography>
|
|
1376
|
-
<Badge variant="secondary">Fallback</Badge>
|
|
1382
|
+
<Badge variant="secondary">{t('settings.email.templates.text.badge', 'Fallback')}</Badge>
|
|
1377
1383
|
</Flex>
|
|
1378
1384
|
<Button
|
|
1379
1385
|
variant="tertiary"
|
|
@@ -1383,14 +1389,14 @@ const SettingsPage = () => {
|
|
|
1383
1389
|
const newTemplates = { ...settings.emailTemplates };
|
|
1384
1390
|
newTemplates[templateKey].text = defaultTemplates[templateKey].text;
|
|
1385
1391
|
handleChange('emailTemplates', newTemplates);
|
|
1386
|
-
toggleNotification({ type: 'success', message: 'Default
|
|
1392
|
+
toggleNotification({ type: 'success', message: t('notifications.success.defaultLoaded', 'Default template loaded!') });
|
|
1387
1393
|
}}
|
|
1388
1394
|
>
|
|
1389
|
-
📋 Load Default
|
|
1395
|
+
📋 {t('settings.email.templates.text.loadDefault', 'Load Default')}
|
|
1390
1396
|
</Button>
|
|
1391
1397
|
</Flex>
|
|
1392
1398
|
<Typography variant="pi" textColor="neutral600" style={{ marginBottom: '16px', display: 'block', fontSize: '14px' }}>
|
|
1393
|
-
Plain text version (no HTML) as fallback for older email clients
|
|
1399
|
+
{t('settings.email.templates.text.description', 'Plain text version (no HTML) as fallback for older email clients')}
|
|
1394
1400
|
</Typography>
|
|
1395
1401
|
<Box
|
|
1396
1402
|
style={{
|
|
@@ -1445,10 +1451,10 @@ const SettingsPage = () => {
|
|
|
1445
1451
|
size="S"
|
|
1446
1452
|
onClick={() => {
|
|
1447
1453
|
navigator.clipboard.writeText(settings.emailTemplates[templateKey].text);
|
|
1448
|
-
toggleNotification({ type: 'success', message: 'Text template copied!' });
|
|
1454
|
+
toggleNotification({ type: 'success', message: t('notifications.success.textCopied', 'Text template copied!') });
|
|
1449
1455
|
}}
|
|
1450
1456
|
>
|
|
1451
|
-
📋 Copy Template
|
|
1457
|
+
📋 {t('settings.email.templates.text.copy', 'Copy Template')}
|
|
1452
1458
|
</Button>
|
|
1453
1459
|
</Flex>
|
|
1454
1460
|
</Box>
|
|
@@ -1470,9 +1476,9 @@ const SettingsPage = () => {
|
|
|
1470
1476
|
<Accordion.Header>
|
|
1471
1477
|
<Accordion.Trigger
|
|
1472
1478
|
icon={Code}
|
|
1473
|
-
description=
|
|
1479
|
+
description={t('settings.webhooks.description', 'Discord & Slack integration')}
|
|
1474
1480
|
>
|
|
1475
|
-
Webhook Integration (Advanced)
|
|
1481
|
+
{t('settings.webhooks.title', 'Webhook Integration (Advanced)')}
|
|
1476
1482
|
</Accordion.Trigger>
|
|
1477
1483
|
</Accordion.Header>
|
|
1478
1484
|
<Accordion.Content>
|
|
@@ -1500,10 +1506,10 @@ const SettingsPage = () => {
|
|
|
1500
1506
|
textColor={settings.enableWebhooks ? 'success700' : 'neutral800'}
|
|
1501
1507
|
style={{ fontSize: '16px' }}
|
|
1502
1508
|
>
|
|
1503
|
-
Enable Webhooks
|
|
1509
|
+
{t('settings.webhooks.enable.title', 'Enable Webhooks')}
|
|
1504
1510
|
</Typography>
|
|
1505
1511
|
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '13px', lineHeight: '1.6' }}>
|
|
1506
|
-
Send session events to Discord, Slack, or custom endpoints
|
|
1512
|
+
{t('settings.webhooks.enable.description', 'Send session events to Discord, Slack, or custom endpoints')}
|
|
1507
1513
|
</Typography>
|
|
1508
1514
|
</Flex>
|
|
1509
1515
|
</Flex>
|
|
@@ -1518,7 +1524,7 @@ const SettingsPage = () => {
|
|
|
1518
1524
|
<Grid.Item col={12}>
|
|
1519
1525
|
<Box>
|
|
1520
1526
|
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '12px', display: 'block' }}>
|
|
1521
|
-
🔗 Discord Webhook URL
|
|
1527
|
+
🔗 {t('settings.webhooks.discord.title', 'Discord Webhook URL')}
|
|
1522
1528
|
</Typography>
|
|
1523
1529
|
<Box
|
|
1524
1530
|
style={{
|
|
@@ -1529,7 +1535,7 @@ const SettingsPage = () => {
|
|
|
1529
1535
|
}}
|
|
1530
1536
|
>
|
|
1531
1537
|
<textarea
|
|
1532
|
-
placeholder=
|
|
1538
|
+
placeholder={t('settings.webhooks.discord.placeholder', 'https://discord.com/api/webhooks/123456789/abcdefghijklmnopqrstuvwxyz...')}
|
|
1533
1539
|
value={settings.discordWebhookUrl}
|
|
1534
1540
|
onChange={(e) => handleChange('discordWebhookUrl', e.target.value)}
|
|
1535
1541
|
rows={3}
|
|
@@ -1550,11 +1556,11 @@ const SettingsPage = () => {
|
|
|
1550
1556
|
</Box>
|
|
1551
1557
|
<Flex justifyContent="space-between" alignItems="center" style={{ marginTop: '10px' }}>
|
|
1552
1558
|
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '12px' }}>
|
|
1553
|
-
Optional: Post session alerts to your Discord channel
|
|
1559
|
+
{t('settings.webhooks.discord.hint', 'Optional: Post session alerts to your Discord channel')}
|
|
1554
1560
|
</Typography>
|
|
1555
1561
|
{settings.discordWebhookUrl && (
|
|
1556
1562
|
<Typography variant="pi" textColor="primary600" style={{ fontSize: '11px', fontFamily: 'monospace' }}>
|
|
1557
|
-
{settings.discordWebhookUrl.length}
|
|
1563
|
+
{t('settings.webhooks.characters', '{count} characters', { count: settings.discordWebhookUrl.length })}
|
|
1558
1564
|
</Typography>
|
|
1559
1565
|
)}
|
|
1560
1566
|
</Flex>
|
|
@@ -1564,7 +1570,7 @@ const SettingsPage = () => {
|
|
|
1564
1570
|
<Grid.Item col={12}>
|
|
1565
1571
|
<Box>
|
|
1566
1572
|
<Typography variant="pi" fontWeight="bold" style={{ marginBottom: '12px', display: 'block' }}>
|
|
1567
|
-
💬 Slack Webhook URL
|
|
1573
|
+
💬 {t('settings.webhooks.slack.title', 'Slack Webhook URL')}
|
|
1568
1574
|
</Typography>
|
|
1569
1575
|
<Box
|
|
1570
1576
|
style={{
|
|
@@ -1575,7 +1581,7 @@ const SettingsPage = () => {
|
|
|
1575
1581
|
}}
|
|
1576
1582
|
>
|
|
1577
1583
|
<textarea
|
|
1578
|
-
placeholder=
|
|
1584
|
+
placeholder={t('settings.webhooks.slack.placeholder', 'https://hooks.slack.com/services/XXXX/XXXX/XXXX')}
|
|
1579
1585
|
value={settings.slackWebhookUrl}
|
|
1580
1586
|
onChange={(e) => handleChange('slackWebhookUrl', e.target.value)}
|
|
1581
1587
|
rows={3}
|
|
@@ -1596,11 +1602,11 @@ const SettingsPage = () => {
|
|
|
1596
1602
|
</Box>
|
|
1597
1603
|
<Flex justifyContent="space-between" alignItems="center" style={{ marginTop: '10px' }}>
|
|
1598
1604
|
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '12px' }}>
|
|
1599
|
-
Optional: Post session alerts to your Slack workspace
|
|
1605
|
+
{t('settings.webhooks.slack.hint', 'Optional: Post session alerts to your Slack workspace')}
|
|
1600
1606
|
</Typography>
|
|
1601
1607
|
{settings.slackWebhookUrl && (
|
|
1602
1608
|
<Typography variant="pi" textColor="primary600" style={{ fontSize: '11px', fontFamily: 'monospace' }}>
|
|
1603
|
-
{settings.slackWebhookUrl.length}
|
|
1609
|
+
{t('settings.webhooks.characters', '{count} characters', { count: settings.slackWebhookUrl.length })}
|
|
1604
1610
|
</Typography>
|
|
1605
1611
|
)}
|
|
1606
1612
|
</Flex>
|
|
@@ -1622,12 +1628,10 @@ const SettingsPage = () => {
|
|
|
1622
1628
|
<Check style={{ width: '20px', height: '20px', color: theme.colors.success[600], flexShrink: 0, marginTop: '2px' }} />
|
|
1623
1629
|
<Box style={{ flex: 1 }}>
|
|
1624
1630
|
<Typography variant="omega" fontWeight="bold" style={{ marginBottom: '8px', display: 'block', color: theme.colors.primary[700] }}>
|
|
1625
|
-
Database-Backed Settings
|
|
1631
|
+
{t('settings.footer.title', 'Database-Backed Settings')}
|
|
1626
1632
|
</Typography>
|
|
1627
1633
|
<Typography variant="pi" textColor="primary700" style={{ fontSize: '13px', lineHeight: '1.8' }}>
|
|
1628
|
-
All settings are stored in your Strapi database and shared across all admin users.
|
|
1629
|
-
Changes take effect immediately - no server restart required!
|
|
1630
|
-
Email templates, webhooks, and security options are all managed from this interface.
|
|
1634
|
+
{t('settings.footer.description', 'All settings are stored in your Strapi database and shared across all admin users. Changes take effect immediately - no server restart required! Email templates, webhooks, and security options are all managed from this interface.')}
|
|
1631
1635
|
</Typography>
|
|
1632
1636
|
</Box>
|
|
1633
1637
|
</Flex>
|