@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.
Files changed (50) hide show
  1. package/dist-lib/components/Plan/PlanForm/PlanForm.js +74 -74
  2. package/dist-lib/components/Plan/PlanForm/PlanForm.js.map +1 -1
  3. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.d.ts +16 -0
  4. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.d.ts.map +1 -0
  5. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.js +115 -0
  6. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.js.map +1 -0
  7. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.module.scss.js +26 -0
  8. package/dist-lib/components/Plan/PlanIntegrity/PlanIntegrity.module.scss.js.map +1 -0
  9. package/dist-lib/components/Plan/PlanPendingBackup/PlanPendingBackup.d.ts +2 -2
  10. package/dist-lib/components/Plan/PlanPendingBackup/PlanPendingBackup.d.ts.map +1 -1
  11. package/dist-lib/components/Plan/PlanPendingBackup/PlanPendingBackup.js.map +1 -1
  12. package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.d.ts +1 -1
  13. package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.d.ts.map +1 -1
  14. package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.js +55 -27
  15. package/dist-lib/components/Settings/GeneralSettings/GeneralSettings.js.map +1 -1
  16. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.d.ts +7 -0
  17. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.d.ts.map +1 -0
  18. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.js +79 -0
  19. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.js.map +1 -0
  20. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.module.scss.js +24 -0
  21. package/dist-lib/components/Settings/TwoFactorSetup/TwoFactorSetup.module.scss.js.map +1 -0
  22. package/dist-lib/components/index.d.ts +2 -0
  23. package/dist-lib/components/index.d.ts.map +1 -1
  24. package/dist-lib/components.js +73 -69
  25. package/dist-lib/components.js.map +1 -1
  26. package/dist-lib/routes/PlanSingle/PlanSingle.d.ts.map +1 -1
  27. package/dist-lib/routes/PlanSingle/PlanSingle.js +123 -98
  28. package/dist-lib/routes/PlanSingle/PlanSingle.js.map +1 -1
  29. package/dist-lib/services/plans.d.ts +6 -0
  30. package/dist-lib/services/plans.d.ts.map +1 -1
  31. package/dist-lib/services/plans.js +151 -127
  32. package/dist-lib/services/plans.js.map +1 -1
  33. package/dist-lib/services/settings.d.ts +16 -0
  34. package/dist-lib/services/settings.d.ts.map +1 -1
  35. package/dist-lib/services/settings.js +147 -68
  36. package/dist-lib/services/settings.js.map +1 -1
  37. package/dist-lib/services.js +92 -84
  38. package/dist-lib/styles/core-frontend.css +1 -1
  39. package/package.json +1 -1
  40. package/src/components/Plan/PlanForm/PlanForm.tsx +14 -14
  41. package/src/components/Plan/PlanIntegrity/PlanIntegrity.module.scss +110 -0
  42. package/src/components/Plan/PlanIntegrity/PlanIntegrity.tsx +187 -0
  43. package/src/components/Plan/PlanPendingBackup/PlanPendingBackup.tsx +2 -2
  44. package/src/components/Settings/GeneralSettings/GeneralSettings.tsx +37 -1
  45. package/src/components/Settings/TwoFactorSetup/TwoFactorSetup.module.scss +62 -0
  46. package/src/components/Settings/TwoFactorSetup/TwoFactorSetup.tsx +102 -0
  47. package/src/components/index.ts +2 -0
  48. package/src/routes/PlanSingle/PlanSingle.tsx +21 -0
  49. package/src/services/plans.ts +30 -0
  50. package/src/services/settings.ts +90 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@plutonhq/core-frontend",
3
3
  "description": "Pluton Core Frontend Library",
4
- "version": "0.1.14",
4
+ "version": "0.1.15",
5
5
  "author": "Plutonhq",
6
6
  "license": "Apache-2.0",
7
7
  "publishConfig": {
@@ -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
- 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
- },
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 PlanPendingBackup {
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 }: PlanPendingBackup) => {
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;
@@ -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
  )}