@plutonhq/core-frontend 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/components/Plan/PlanForm/PlanForm.js +74 -74
- package/dist-lib/components/Plan/PlanForm/PlanForm.js.map +1 -1
- package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.d.ts +16 -0
- package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.d.ts.map +1 -0
- package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.js +115 -0
- package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.js.map +1 -0
- package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.module.scss.js +26 -0
- package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.module.scss.js.map +1 -0
- package/dist-lib/components/Plan/PlanPendingBackup/PlanPendingBackup.d.ts +2 -2
- package/dist-lib/components/Plan/PlanPendingBackup/PlanPendingBackup.d.ts.map +1 -1
- package/dist-lib/components/Plan/PlanPendingBackup/PlanPendingBackup.js.map +1 -1
- package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.d.ts +1 -1
- package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.d.ts.map +1 -1
- package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.js +55 -27
- package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.js.map +1 -1
- package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.d.ts +7 -0
- package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.d.ts.map +1 -0
- package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.js +79 -0
- package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.js.map +1 -0
- package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.module.scss.js +24 -0
- package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.module.scss.js.map +1 -0
- package/dist-lib/components/index.d.ts +2 -0
- package/dist-lib/components/index.d.ts.map +1 -1
- package/dist-lib/components.js +73 -69
- package/dist-lib/components.js.map +1 -1
- package/dist-lib/routes/PlanSingle/PlanSingle.d.ts.map +1 -1
- package/dist-lib/routes/PlanSingle/PlanSingle.js +123 -98
- package/dist-lib/routes/PlanSingle/PlanSingle.js.map +1 -1
- package/dist-lib/services/plans.d.ts +6 -0
- package/dist-lib/services/plans.d.ts.map +1 -1
- package/dist-lib/services/plans.js +151 -127
- package/dist-lib/services/plans.js.map +1 -1
- package/dist-lib/services/settings.d.ts +16 -0
- package/dist-lib/services/settings.d.ts.map +1 -1
- package/dist-lib/services/settings.js +147 -68
- package/dist-lib/services/settings.js.map +1 -1
- package/dist-lib/services.js +92 -84
- package/dist-lib/styles/core-frontend.css +1 -1
- package/package.json +1 -1
- package/src/components/Plan/PlanForm/PlanForm.tsx +14 -14
- package/src/components/Plan/PlanIntegrity/PlanIntegrity.module.scss +110 -0
- package/src/components/Plan/PlanIntegrity/PlanIntegrity.tsx +187 -0
- package/src/components/Plan/PlanPendingBackup/PlanPendingBackup.tsx +2 -2
- package/src/components/Settings/GeneralSettings/GeneralSettings.tsx +37 -1
- package/src/components/Settings/TwoFactorSetup/TwoFactorSetup.module.scss +62 -0
- package/src/components/Settings/TwoFactorSetup/TwoFactorSetup.tsx +102 -0
- package/src/components/index.ts +2 -0
- package/src/routes/PlanSingle/PlanSingle.tsx +21 -0
- package/src/services/plans.ts +30 -0
- package/src/services/settings.ts +90 -0
package/package.json
CHANGED
|
@@ -190,20 +190,20 @@ const PlanForm = ({
|
|
|
190
190
|
label: 'Incremental Backup',
|
|
191
191
|
description: 'Periodically create Incremental backup snapshots of source',
|
|
192
192
|
},
|
|
193
|
-
{
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
},
|
|
193
|
+
// {
|
|
194
|
+
// value: 'sync',
|
|
195
|
+
// icon: 'reload',
|
|
196
|
+
// label: 'Real-time Sync',
|
|
197
|
+
// description: 'Maintain identical source (with revisions)',
|
|
198
|
+
// disabled: true,
|
|
199
|
+
// },
|
|
200
|
+
// {
|
|
201
|
+
// value: 'rescue',
|
|
202
|
+
// icon: 'rescue',
|
|
203
|
+
// label: 'Linux Server Backup',
|
|
204
|
+
// description: 'Full Linux system backups with bootable ISO image',
|
|
205
|
+
// disabled: true,
|
|
206
|
+
// },
|
|
207
207
|
]}
|
|
208
208
|
/>
|
|
209
209
|
)}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
.integrityMessage {
|
|
2
|
+
:global(.label) {
|
|
3
|
+
font-size: 0.9em;
|
|
4
|
+
border-radius: 6px;
|
|
5
|
+
padding: 10px;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.fixSuggestion {
|
|
10
|
+
border-radius: 6px;
|
|
11
|
+
border: 1px solid var(--line-color);
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
margin: 0 5px;
|
|
14
|
+
margin-top: 12px;
|
|
15
|
+
h4 {
|
|
16
|
+
padding: 12px;
|
|
17
|
+
font-weight: 600;
|
|
18
|
+
margin: 0;
|
|
19
|
+
border-bottom: 1px solid var(--line-color);
|
|
20
|
+
}
|
|
21
|
+
.fixSuggestionContent {
|
|
22
|
+
padding: 0 12px;
|
|
23
|
+
font-size: 0.9em;
|
|
24
|
+
code {
|
|
25
|
+
background: var(--line-color);
|
|
26
|
+
padding: 5px;
|
|
27
|
+
border-radius: 4px;
|
|
28
|
+
display: block;
|
|
29
|
+
font-family: monospace;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.integrityLogs {
|
|
35
|
+
border-radius: 6px;
|
|
36
|
+
border: 1px solid var(--line-color);
|
|
37
|
+
box-sizing: border-box;
|
|
38
|
+
margin: 0 5px;
|
|
39
|
+
margin-top: 12px;
|
|
40
|
+
|
|
41
|
+
.integrityLogsHeader {
|
|
42
|
+
padding: 12px;
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
border-bottom: 1px solid var(--line-color);
|
|
45
|
+
display: flex;
|
|
46
|
+
justify-content: space-between;
|
|
47
|
+
& > div:nth-child(2) {
|
|
48
|
+
font-weight: normal;
|
|
49
|
+
color: var(--content-text-color-light);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
.integrityLogMessages {
|
|
53
|
+
font-family: monospace;
|
|
54
|
+
background-color: var(--background-color);
|
|
55
|
+
line-height: 1.3rem;
|
|
56
|
+
max-height: 300px;
|
|
57
|
+
overflow: auto;
|
|
58
|
+
span {
|
|
59
|
+
display: block;
|
|
60
|
+
padding: 4px 12px;
|
|
61
|
+
line-break: anywhere;
|
|
62
|
+
&:nth-child(even) {
|
|
63
|
+
background: rgba(0, 0, 0, 0.03);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.integrityResult {
|
|
70
|
+
&.withReplications {
|
|
71
|
+
border: 1px solid var(--line-color);
|
|
72
|
+
border-radius: 6px;
|
|
73
|
+
.replicationResult {
|
|
74
|
+
h4 {
|
|
75
|
+
margin: 0;
|
|
76
|
+
padding: 12px;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
display: flex;
|
|
79
|
+
justify-content: space-between;
|
|
80
|
+
border-bottom: 1px solid var(--line-color);
|
|
81
|
+
i {
|
|
82
|
+
background-color: var(--background-color);
|
|
83
|
+
color: var(--content-text-color-light);
|
|
84
|
+
padding: 1px 12px;
|
|
85
|
+
border-radius: 20px;
|
|
86
|
+
font-size: 0.7rem;
|
|
87
|
+
font-weight: normal;
|
|
88
|
+
font-style: normal;
|
|
89
|
+
display: inline-block;
|
|
90
|
+
margin-left: 6px;
|
|
91
|
+
}
|
|
92
|
+
img {
|
|
93
|
+
max-width: 14px;
|
|
94
|
+
vertical-align: middle;
|
|
95
|
+
margin-right: 6px;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
.replicationResultContent {
|
|
99
|
+
padding: 12px;
|
|
100
|
+
border-bottom: 1px solid var(--line-color);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@media only screen and (max-width: 768px) {
|
|
107
|
+
.integrityLogs .integrityLogsHeader {
|
|
108
|
+
flex-direction: column;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import classes from './PlanIntegrity.module.scss';
|
|
3
|
+
import { useCheckPlanIntegrity } from '../../../services';
|
|
4
|
+
import { Plan, PlanReplicationStorage, PlanVerifiedResult } from '../../../@types';
|
|
5
|
+
import { formatDateTime, timeAgo } from '../../../utils';
|
|
6
|
+
import Icon from '../../common/Icon/Icon';
|
|
7
|
+
import ActionModal from '../../common/ActionModal/ActionModal';
|
|
8
|
+
|
|
9
|
+
interface PlanIntegrityProps {
|
|
10
|
+
planId: string;
|
|
11
|
+
taskPending: boolean;
|
|
12
|
+
verificationData: Plan['verified'];
|
|
13
|
+
storage: { name: string; type: string; id: string };
|
|
14
|
+
replicationStorages: PlanReplicationStorage[];
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PlanIntegrity = ({ planId, taskPending, verificationData, storage, replicationStorages = [], onClose }: PlanIntegrityProps) => {
|
|
19
|
+
const [showContent, setShowContent] = useState('primary');
|
|
20
|
+
const integrityCheckMutation = useCheckPlanIntegrity();
|
|
21
|
+
|
|
22
|
+
const integrityResultLoaded = verificationData && verificationData?.result;
|
|
23
|
+
const hasReplications = replicationStorages && replicationStorages.length > 0;
|
|
24
|
+
|
|
25
|
+
const getBackupResultByStorage = (storageType: string): PlanVerifiedResult | null => {
|
|
26
|
+
if (!verificationData?.result) return null;
|
|
27
|
+
|
|
28
|
+
const replicationResult = verificationData.result as Record<string, PlanVerifiedResult>;
|
|
29
|
+
return replicationResult[storageType] ?? null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const renderLogs = (storageType: string) => {
|
|
33
|
+
if (integrityCheckMutation.isPending) return null;
|
|
34
|
+
const backupResData = getBackupResultByStorage(storageType);
|
|
35
|
+
const messages = backupResData?.logs;
|
|
36
|
+
if (!messages?.length) return null;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={classes.integrityLogs}>
|
|
40
|
+
<div className={classes.integrityLogsHeader}>
|
|
41
|
+
<div>Integrity Check Output Logs</div>
|
|
42
|
+
<div title={formatDateTime(verificationData.endedAt || verificationData.startedAt)}>
|
|
43
|
+
<Icon type="clock" size={14} /> {timeAgo(new Date(verificationData.endedAt || verificationData.startedAt))}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div className={classes.integrityLogMessages}>
|
|
47
|
+
{messages.map((message, index) => (
|
|
48
|
+
<span key={index}>{message}</span>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const renderBackupFixSuggestion = (storageType: string) => {
|
|
56
|
+
if (!integrityResultLoaded) return null;
|
|
57
|
+
if (integrityCheckMutation.isPending) return null;
|
|
58
|
+
const backupResData = getBackupResultByStorage(storageType);
|
|
59
|
+
if (!backupResData?.hasError) return null;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={classes.fixSuggestion}>
|
|
63
|
+
<h4>Fix Suggestion</h4>
|
|
64
|
+
<div className={classes.fixSuggestionContent}>
|
|
65
|
+
{backupResData.fix &&
|
|
66
|
+
backupResData.fix
|
|
67
|
+
.split('\n')
|
|
68
|
+
.map((line, index) => <p key={index}>{line.includes('`') ? <code>{line.replace(/`/g, '')}</code> : line}</p>)}
|
|
69
|
+
{!backupResData.fix && <p>No Suggestion for this issue.</p>}
|
|
70
|
+
<p>
|
|
71
|
+
Learn more about fixing restic repo issues{' '}
|
|
72
|
+
<a
|
|
73
|
+
href="https://restic.readthedocs.io/en/latest/077_troubleshooting.html#find-out-what-is-damaged"
|
|
74
|
+
target="_blank"
|
|
75
|
+
rel="noreferrer"
|
|
76
|
+
>
|
|
77
|
+
here.
|
|
78
|
+
</a>
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const renderBackupStatus = (storageType: string) => {
|
|
86
|
+
if (!integrityResultLoaded) return null;
|
|
87
|
+
if (integrityCheckMutation.isPending) return null;
|
|
88
|
+
const backupResData = getBackupResultByStorage(storageType);
|
|
89
|
+
const backupHasError = backupResData?.hasError;
|
|
90
|
+
const backupMessage = backupResData?.message;
|
|
91
|
+
|
|
92
|
+
if (!backupHasError && backupMessage?.includes('No Issue Detected')) {
|
|
93
|
+
return <div className="label success">🥳 Yoohoo! No Corruption or Bit rot found. Your backup snapshots are completely restorable.</div>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return <div className="label error">⛔ Error Found. {backupMessage}</div>;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const renderResultWithReplications = () => {
|
|
100
|
+
return (
|
|
101
|
+
<div className={`${classes.integrityResult} ${classes.withReplications}`}>
|
|
102
|
+
{verificationData &&
|
|
103
|
+
Object.entries(verificationData.result as Record<string, PlanVerifiedResult>).map(([storageType]) => {
|
|
104
|
+
let theStorage = storage;
|
|
105
|
+
if (storageType !== 'primary') {
|
|
106
|
+
const mirror = replicationStorages.find((s) => s.storageId === storageType.replace('mirror_', ''));
|
|
107
|
+
if (mirror) {
|
|
108
|
+
theStorage = { name: mirror.storageName as string, type: mirror?.storageType as string, id: mirror?.storageId as string };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!theStorage) return null;
|
|
112
|
+
const backupResData = getBackupResultByStorage(storageType);
|
|
113
|
+
const backupHasError = backupResData?.hasError;
|
|
114
|
+
return (
|
|
115
|
+
<div key={storageType} className={classes.replicationResult}>
|
|
116
|
+
<h4 onClick={() => setShowContent(storageType)} className={showContent === storageType ? classes.active : ''}>
|
|
117
|
+
<span>
|
|
118
|
+
<img src={`/providers/${theStorage.type}.png`} />
|
|
119
|
+
{theStorage.name} <i>{storageType === 'primary' ? 'Primary' : `Mirror`}</i>
|
|
120
|
+
</span>
|
|
121
|
+
<span>
|
|
122
|
+
{backupHasError ? (
|
|
123
|
+
<Icon type="error-circle-filled" size={14} color="#ff4d4f" />
|
|
124
|
+
) : (
|
|
125
|
+
<Icon type="check-circle-filled" size={14} color="#06ba9f" />
|
|
126
|
+
)}
|
|
127
|
+
</span>
|
|
128
|
+
</h4>
|
|
129
|
+
{showContent === storageType && (
|
|
130
|
+
<div className={classes.replicationResultContent}>
|
|
131
|
+
{renderBackupStatus(storageType)}
|
|
132
|
+
{renderLogs(storageType)}
|
|
133
|
+
{renderBackupFixSuggestion(storageType)}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<ActionModal
|
|
145
|
+
title="Check Backup Integrity"
|
|
146
|
+
message={
|
|
147
|
+
<div className={classes.integrityMessage}>
|
|
148
|
+
{!integrityResultLoaded && <p>Check the integrity of the backup snapshots now to check if they are restorable?</p>}
|
|
149
|
+
{integrityResultLoaded && (
|
|
150
|
+
<div>
|
|
151
|
+
{!hasReplications ? (
|
|
152
|
+
<div className={classes.integrityResult}>
|
|
153
|
+
{renderBackupStatus('primary')}
|
|
154
|
+
{renderLogs('primary')}
|
|
155
|
+
{renderBackupFixSuggestion('primary')}
|
|
156
|
+
</div>
|
|
157
|
+
) : (
|
|
158
|
+
renderResultWithReplications()
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
{integrityCheckMutation.isPending && (
|
|
163
|
+
<div className="label in_progress">
|
|
164
|
+
<Icon type="loading" size={14} />
|
|
165
|
+
Checking integrity...
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
}
|
|
170
|
+
errorMessage={integrityCheckMutation.error?.message}
|
|
171
|
+
closeModal={() => onClose()}
|
|
172
|
+
width="600px"
|
|
173
|
+
primaryAction={{
|
|
174
|
+
title: integrityResultLoaded ? 'Check Again' : 'Check Now',
|
|
175
|
+
type: 'default',
|
|
176
|
+
isPending: integrityCheckMutation.isPending,
|
|
177
|
+
action: () => !taskPending && integrityCheckMutation.mutate({ planId }),
|
|
178
|
+
}}
|
|
179
|
+
secondaryAction={{
|
|
180
|
+
title: 'Close',
|
|
181
|
+
action: () => onClose(),
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export default PlanIntegrity;
|
|
@@ -4,13 +4,13 @@ import { useCheckActiveBackupsOrRestore } from '../../../services/plans';
|
|
|
4
4
|
import classes from './PlanPendingBackup.module.scss';
|
|
5
5
|
import Icon from '../../common/Icon/Icon';
|
|
6
6
|
|
|
7
|
-
interface
|
|
7
|
+
interface PlanPendingBackupProps {
|
|
8
8
|
planId: string;
|
|
9
9
|
type?: 'backup' | 'restore';
|
|
10
10
|
onPendingDetect: () => void;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const PlanPendingBackup = ({ planId, type = 'backup', onPendingDetect }:
|
|
13
|
+
const PlanPendingBackup = ({ planId, type = 'backup', onPendingDetect }: PlanPendingBackupProps) => {
|
|
14
14
|
const [, setSearchParams] = useSearchParams();
|
|
15
15
|
const checkActivesMutation = useCheckActiveBackupsOrRestore();
|
|
16
16
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { ActionModal } from '../..';
|
|
1
3
|
import { useTheme } from '../../../context/ThemeContext';
|
|
2
4
|
import Input from '../../common/form/Input/Input';
|
|
5
|
+
import Toggle from '../../common/form/Toggle/Toggle';
|
|
3
6
|
import Tristate from '../../common/form/Tristate/Tristate';
|
|
7
|
+
import TwoFactorSetup from '../TwoFactorSetup/TwoFactorSetup';
|
|
4
8
|
import classes from './GeneralSettings.module.scss';
|
|
5
9
|
|
|
6
10
|
interface GeneralSettingsProps {
|
|
@@ -9,8 +13,22 @@ interface GeneralSettingsProps {
|
|
|
9
13
|
onUpdate: (settings: Record<string, any>) => void;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
const GeneralSettings = ({ settings, onUpdate }: GeneralSettingsProps) => {
|
|
16
|
+
const GeneralSettings = ({ settings, settingsID, onUpdate }: GeneralSettingsProps) => {
|
|
13
17
|
const { setTheme } = useTheme();
|
|
18
|
+
const [show2FASetupConfirm, setShow2FASetupConfirm] = useState(false);
|
|
19
|
+
const [show2FASetup, setShow2FASetup] = useState(false);
|
|
20
|
+
|
|
21
|
+
const { totp } = settings || {};
|
|
22
|
+
const is2FASetupComplete = totp?.secret;
|
|
23
|
+
|
|
24
|
+
const update2FASetting = (enabled: boolean) => {
|
|
25
|
+
if (enabled === true && !is2FASetupComplete) {
|
|
26
|
+
setShow2FASetupConfirm(true);
|
|
27
|
+
return;
|
|
28
|
+
} else {
|
|
29
|
+
onUpdate({ ...settings, totp: { ...totp, enabled } });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
14
32
|
|
|
15
33
|
const handleThemeChange = (newThemeValue: 'auto' | 'light' | 'dark') => {
|
|
16
34
|
setTheme(newThemeValue);
|
|
@@ -58,6 +76,24 @@ const GeneralSettings = ({ settings, onUpdate }: GeneralSettingsProps) => {
|
|
|
58
76
|
inline={false}
|
|
59
77
|
/>
|
|
60
78
|
</div>
|
|
79
|
+
<div className={classes.field}>
|
|
80
|
+
<Toggle label="Enable 2FA" fieldValue={settings?.totp?.enabled || false} onUpdate={(val) => update2FASetting(val)} inline={true} />
|
|
81
|
+
</div>
|
|
82
|
+
{show2FASetupConfirm && (
|
|
83
|
+
<ActionModal
|
|
84
|
+
title={`Enable Two-Factor Authentication (2FA)`}
|
|
85
|
+
message={`Are you sure you want to enable Two-Factor Authentication (2FA) to secure Pluton Login? You will be required to use an authenticator app to login if you enable this feature.`}
|
|
86
|
+
closeModal={() => setShow2FASetupConfirm(false)}
|
|
87
|
+
width="420px"
|
|
88
|
+
primaryAction={{
|
|
89
|
+
title: `Yes, Enable 2FA`,
|
|
90
|
+
type: 'default',
|
|
91
|
+
isPending: false,
|
|
92
|
+
action: () => setShow2FASetup(true),
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
{show2FASetup && <TwoFactorSetup id={settingsID} close={() => setShow2FASetup(false)} />}
|
|
61
97
|
</div>
|
|
62
98
|
);
|
|
63
99
|
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
.setupContainer {
|
|
2
|
+
.errorMsg {
|
|
3
|
+
background-color: var(--error-bg-color);
|
|
4
|
+
color: var(--error-text-color);
|
|
5
|
+
padding: 6px 12px;
|
|
6
|
+
border-radius: 6px;
|
|
7
|
+
}
|
|
8
|
+
.instructions {
|
|
9
|
+
ol {
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 12px 20px;
|
|
12
|
+
padding-top: 0;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
.qrSection {
|
|
16
|
+
.qrCode {
|
|
17
|
+
background-color: var(--background-color);
|
|
18
|
+
padding: 12px;
|
|
19
|
+
text-align: center;
|
|
20
|
+
border-radius: 6px 6px 0 0;
|
|
21
|
+
}
|
|
22
|
+
.setupKey {
|
|
23
|
+
padding: 6px;
|
|
24
|
+
margin-bottom: 12px;
|
|
25
|
+
background: var(--primary-color-light);
|
|
26
|
+
text-align: center;
|
|
27
|
+
border-radius: 0 0 6px 6px;
|
|
28
|
+
}
|
|
29
|
+
.verifySection {
|
|
30
|
+
display: flex;
|
|
31
|
+
& > div:nth-child(1) {
|
|
32
|
+
width: calc(100% - 140px);
|
|
33
|
+
}
|
|
34
|
+
& > button {
|
|
35
|
+
margin-left: 12px;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.successMessage {
|
|
42
|
+
.successMsg {
|
|
43
|
+
background-color: var(--success-bg-color);
|
|
44
|
+
color: var(--success-text-color);
|
|
45
|
+
padding: 6px 12px;
|
|
46
|
+
border-radius: 6px;
|
|
47
|
+
font-weight: 600;
|
|
48
|
+
}
|
|
49
|
+
strong {
|
|
50
|
+
font-weight: 600;
|
|
51
|
+
}
|
|
52
|
+
code {
|
|
53
|
+
background-color: var(--line-color);
|
|
54
|
+
width: 100%;
|
|
55
|
+
display: block;
|
|
56
|
+
padding: 12px;
|
|
57
|
+
box-sizing: border-box;
|
|
58
|
+
border-radius: 6px;
|
|
59
|
+
white-space: pre;
|
|
60
|
+
font-size: 14px;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useSetupTwoFactorAuth, useVerifyTwoFactorAuth } from '../../../services';
|
|
3
|
+
import Icon from '../../common/Icon/Icon';
|
|
4
|
+
import Modal from '../../common/Modal/Modal';
|
|
5
|
+
import Input from '../../common/form/Input/Input';
|
|
6
|
+
import Button from '../../common/Button/Button';
|
|
7
|
+
import classes from './TwoFactorSetup.module.scss';
|
|
8
|
+
|
|
9
|
+
interface TwoFactorSetupProps {
|
|
10
|
+
id: number;
|
|
11
|
+
close: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const TwoFactorSetup = ({ close, id = 1 }: TwoFactorSetupProps) => {
|
|
15
|
+
const [verificationCode, setVerificationCode] = useState('');
|
|
16
|
+
const setupMutation = useSetupTwoFactorAuth();
|
|
17
|
+
const verifyMutation = useVerifyTwoFactorAuth();
|
|
18
|
+
const isLoading = setupMutation.isPending || verifyMutation.isPending;
|
|
19
|
+
|
|
20
|
+
const qrCodeUrl: string = setupMutation.data?.result?.qrCodeDataUrl || '';
|
|
21
|
+
const setupKey: string = setupMutation.data?.result?.setupKey || '';
|
|
22
|
+
const recoveryCodes: string[] = verifyMutation.data?.result?.recoveryCodes || [];
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setupMutation.mutate(id);
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const verify2FA = () => {
|
|
29
|
+
verifyMutation.mutate({ code: verificationCode, id });
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
{!verifyMutation.isSuccess && (
|
|
35
|
+
<Modal title="Two-Factor Authentication (2FA) Setup" closeModal={() => !isLoading && close()} width="500px" disableBackdropClick={true}>
|
|
36
|
+
<div className={classes.twoFactorSetup}>
|
|
37
|
+
{setupMutation.isPending && (
|
|
38
|
+
<p>
|
|
39
|
+
<Icon type="loading" /> Setting up 2FA...
|
|
40
|
+
</p>
|
|
41
|
+
)}
|
|
42
|
+
{setupMutation.isError && <p className={classes.error}>Error setting up 2FA. Please try again.</p>}
|
|
43
|
+
{setupMutation.isSuccess && (
|
|
44
|
+
<div className={classes.setupContainer}>
|
|
45
|
+
<div className={classes.instructions}>
|
|
46
|
+
<h4>Setup Instructions</h4>
|
|
47
|
+
<ol>
|
|
48
|
+
<li>
|
|
49
|
+
Download and install an authenticator app on your mobile device (e.g., Google Authenticator, Authy, Microsoft
|
|
50
|
+
Authenticator).
|
|
51
|
+
</li>
|
|
52
|
+
<li>Open the authenticator app and choose to add a new account.</li>
|
|
53
|
+
<li>Scan the QR code below using the authenticator app or manually enter the setup key provided.</li>
|
|
54
|
+
<li>Once added, the app will generate a 6-digit verification code that refreshes every 30 seconds.</li>
|
|
55
|
+
<li>Enter the current 6-digit code from the authenticator app into the field below to complete the setup.</li>
|
|
56
|
+
</ol>
|
|
57
|
+
</div>
|
|
58
|
+
<div className={classes.qrSection}>
|
|
59
|
+
<div className={classes.qrCode}>{qrCodeUrl && <img src={qrCodeUrl} alt="2FA QR Code" />}</div>
|
|
60
|
+
<div className={classes.setupKey}>Setup Key: {setupKey}</div>
|
|
61
|
+
{verifyMutation.isError && <p className={classes.errorMsg}>{verifyMutation.error?.message}</p>}
|
|
62
|
+
<div className={classes.verifySection}>
|
|
63
|
+
<Input
|
|
64
|
+
type="text"
|
|
65
|
+
placeholder="Enter 6-digit code"
|
|
66
|
+
fieldValue={verificationCode}
|
|
67
|
+
onUpdate={(val) => setVerificationCode(val)}
|
|
68
|
+
full={true}
|
|
69
|
+
/>
|
|
70
|
+
<Button text="Verify & Enable 2FA" onClick={() => verify2FA()} variant="primary" size="sm" />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
</Modal>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{verifyMutation.isSuccess && (
|
|
80
|
+
<Modal
|
|
81
|
+
title="Two-Factor Authentication (2FA) Setup"
|
|
82
|
+
closeModal={() => !isLoading && location.reload()}
|
|
83
|
+
width="500px"
|
|
84
|
+
disableBackdropClick={true}
|
|
85
|
+
>
|
|
86
|
+
<div className={classes.successMessage}>
|
|
87
|
+
<p className={classes.successMsg}>Two-Factor Authentication (2FA) has been successfully enabled.</p>
|
|
88
|
+
<p>
|
|
89
|
+
<strong>
|
|
90
|
+
Save these backup codes in a secure place. They will be required to access your account if you lose your device. They won't be
|
|
91
|
+
shown again.
|
|
92
|
+
</strong>
|
|
93
|
+
</p>
|
|
94
|
+
<code className={classes.recoveryCodes}>{recoveryCodes.join('\n')}</code>
|
|
95
|
+
</div>
|
|
96
|
+
</Modal>
|
|
97
|
+
)}
|
|
98
|
+
</>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export default TwoFactorSetup;
|
package/src/components/index.ts
CHANGED
|
@@ -88,6 +88,7 @@ export { default as PlanStrategySettings } from './Plan/PlanSettings/PlanStrateg
|
|
|
88
88
|
export { default as PlanTypeSettings } from './Plan/PlanSettings/PlanTypeSettings';
|
|
89
89
|
export { default as PlanStats } from './Plan/PlanStats/PlanStats';
|
|
90
90
|
export { default as PlanUnlockModal } from './Plan/PlanUnlockModal/PlanUnlockModal';
|
|
91
|
+
export { default as PlanIntegrity } from './Plan/PlanIntegrity/PlanIntegrity';
|
|
91
92
|
|
|
92
93
|
// Mirror/Replication components
|
|
93
94
|
export { default as MirrorStatusBadge } from './Plan/Mirrors/MirrorStatusBadge';
|
|
@@ -111,6 +112,7 @@ export { default as AppLogs } from './Settings/AppLogs/AppLogs';
|
|
|
111
112
|
export { default as GeneralSettings } from './Settings/GeneralSettings/GeneralSettings';
|
|
112
113
|
export { default as IntegrationSettings } from './Settings/IntegrationSettings/IntegrationSettings';
|
|
113
114
|
export { default as SMTPSettings } from './Settings/IntegrationSettings/SMTPSettings';
|
|
115
|
+
export { default as TwoFactorSetup } from './Settings/TwoFactorSetup/TwoFactorSetup';
|
|
114
116
|
|
|
115
117
|
// Skeleton components
|
|
116
118
|
export { default as SkeletonItems } from './Skeleton/SkeletonItems';
|
|
@@ -14,9 +14,11 @@ import PlanUnlockModal from '../../components/Plan/PlanUnlockModal/PlanUnlockMod
|
|
|
14
14
|
import PlanRemoveModal from '../../components/Plan/PlanRemoveModal/PlanRemoveModal';
|
|
15
15
|
import PlanProgress from '../../components/Plan/PlanProgress/PlanProgress';
|
|
16
16
|
import PlanBackups from '../../components/Plan/PlanBackups/PlanBackups';
|
|
17
|
+
import PlanIntegrity from '../../components/Plan/PlanIntegrity/PlanIntegrity';
|
|
17
18
|
|
|
18
19
|
const PlanSingle = () => {
|
|
19
20
|
const [showMoreOptions, setShowMoreOptions] = useState(false);
|
|
21
|
+
const [showIntegrityModal, setShowIntegrityModal] = useState(false);
|
|
20
22
|
const { id } = useParams();
|
|
21
23
|
|
|
22
24
|
const EditPlanModal = useComponentOverride('EditPlan', EditPlan);
|
|
@@ -126,6 +128,15 @@ const PlanSingle = () => {
|
|
|
126
128
|
<Icon size={14} type="unlock" /> Unlock
|
|
127
129
|
</li>
|
|
128
130
|
)}
|
|
131
|
+
<li
|
|
132
|
+
className={classes.actionBtn}
|
|
133
|
+
onClick={() => {
|
|
134
|
+
setShowIntegrityModal(true);
|
|
135
|
+
setShowMoreOptions(false);
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<Icon size={14} type="integrity" /> Check Integrity
|
|
139
|
+
</li>
|
|
129
140
|
<li
|
|
130
141
|
className={classes.actionBtn}
|
|
131
142
|
onClick={() => {
|
|
@@ -174,6 +185,16 @@ const PlanSingle = () => {
|
|
|
174
185
|
close={() => setShowPruneModal(false)}
|
|
175
186
|
/>
|
|
176
187
|
)}
|
|
188
|
+
{showIntegrityModal && !isSync && (
|
|
189
|
+
<PlanIntegrity
|
|
190
|
+
planId={plan.id}
|
|
191
|
+
taskPending={taskPending}
|
|
192
|
+
verificationData={plan.verified}
|
|
193
|
+
storage={plan.storage}
|
|
194
|
+
replicationStorages={plan.settings.replication?.enabled ? plan.settings.replication.storages : []}
|
|
195
|
+
onClose={() => setShowIntegrityModal(false)}
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
177
198
|
{showDeleteModal && (
|
|
178
199
|
<PlanRemoveModal planId={id} taskPending={taskPending} actionInProgress={actionInProgress} close={() => setShowDeleteModal(false)} />
|
|
179
200
|
)}
|