@visns-studio/visns-components 5.4.7 → 5.4.8
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 +527 -67
- package/package.json +2 -1
- package/src/components/crm/Navigation.jsx +19 -4
- package/src/components/crm/auth/Login.jsx +38 -4
- package/src/components/crm/auth/Profile.jsx +861 -176
- package/src/components/crm/auth/TwoFactorAuth.jsx +223 -60
- package/src/components/crm/auth/styles/Login.module.scss +36 -0
- package/src/components/crm/auth/styles/Profile.module.scss +469 -20
- package/src/components/crm/auth/styles/TwoFactorAuth.module.scss +37 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// src/components/Profile.jsx
|
|
1
|
+
// src/components/crm/auth/Profile.jsx
|
|
2
2
|
import '../../styles/global.css';
|
|
3
3
|
|
|
4
|
-
import React, { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import React, { useState, useEffect, useRef, useCallback, memo } from 'react';
|
|
5
5
|
import PasswordStrengthBar from 'react-password-strength-bar';
|
|
6
6
|
import { toast } from 'react-toastify';
|
|
7
7
|
import SignaturePad from 'react-signature-pad-wrapper';
|
|
@@ -9,8 +9,59 @@ import CustomFetch from '../Fetch';
|
|
|
9
9
|
import TableFilter from '../TableFilter';
|
|
10
10
|
import styles from './styles/Profile.module.scss';
|
|
11
11
|
|
|
12
|
+
// Form field component to reduce repetition
|
|
13
|
+
const FormField = memo(
|
|
14
|
+
({
|
|
15
|
+
type = 'text',
|
|
16
|
+
name,
|
|
17
|
+
value,
|
|
18
|
+
onChange,
|
|
19
|
+
label,
|
|
20
|
+
readOnly = false,
|
|
21
|
+
error = '',
|
|
22
|
+
children,
|
|
23
|
+
}) => {
|
|
24
|
+
const isPrefocused = type === 'password';
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={styles.formItem}>
|
|
28
|
+
<label className={styles.fi__label}>
|
|
29
|
+
{!isPrefocused && (
|
|
30
|
+
<input
|
|
31
|
+
type={type}
|
|
32
|
+
name={name}
|
|
33
|
+
onChange={onChange}
|
|
34
|
+
className={`${styles.input} ${error}`}
|
|
35
|
+
value={value}
|
|
36
|
+
readOnly={readOnly}
|
|
37
|
+
/>
|
|
38
|
+
)}
|
|
39
|
+
<span
|
|
40
|
+
className={`${styles.fi__span} ${
|
|
41
|
+
isPrefocused ? styles.prefocus : ''
|
|
42
|
+
}`}
|
|
43
|
+
>
|
|
44
|
+
{label}
|
|
45
|
+
</span>
|
|
46
|
+
{isPrefocused && (
|
|
47
|
+
<input
|
|
48
|
+
type={type}
|
|
49
|
+
name={name}
|
|
50
|
+
onChange={onChange}
|
|
51
|
+
value={value}
|
|
52
|
+
className={`${styles.input} ${error}`}
|
|
53
|
+
/>
|
|
54
|
+
)}
|
|
55
|
+
{children}
|
|
56
|
+
</label>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
12
62
|
const Profile = ({ userProfile, setUserProfile }) => {
|
|
13
63
|
const signatureRef = useRef(null);
|
|
64
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
14
65
|
const [profile, setProfile] = useState({
|
|
15
66
|
name: '',
|
|
16
67
|
email: '',
|
|
@@ -25,6 +76,16 @@ const Profile = ({ userProfile, setUserProfile }) => {
|
|
|
25
76
|
password_confirmation: '',
|
|
26
77
|
});
|
|
27
78
|
|
|
79
|
+
// 2FA related states
|
|
80
|
+
const [twoFactorEnabled, setTwoFactorEnabled] = useState(false);
|
|
81
|
+
const [twoFactorData, setTwoFactorData] = useState({
|
|
82
|
+
qrCodeUrl: '',
|
|
83
|
+
secretKey: '',
|
|
84
|
+
recoveryCodes: [],
|
|
85
|
+
});
|
|
86
|
+
const [verificationCode, setVerificationCode] = useState('');
|
|
87
|
+
const [showTwoFactorSetup, setShowTwoFactorSetup] = useState(false);
|
|
88
|
+
|
|
28
89
|
const [filters, setFilters] = useState([
|
|
29
90
|
{
|
|
30
91
|
id: 'overview',
|
|
@@ -32,222 +93,846 @@ const Profile = ({ userProfile, setUserProfile }) => {
|
|
|
32
93
|
label: 'Overview',
|
|
33
94
|
class: 'subactive',
|
|
34
95
|
},
|
|
96
|
+
{
|
|
97
|
+
id: '2fa',
|
|
98
|
+
show: false,
|
|
99
|
+
label: 'Two-Factor Authentication',
|
|
100
|
+
class: '',
|
|
101
|
+
},
|
|
35
102
|
]);
|
|
36
103
|
|
|
37
|
-
const handleClearSignature = (e) => {
|
|
104
|
+
const handleClearSignature = useCallback((e) => {
|
|
38
105
|
if (e) {
|
|
39
106
|
e.preventDefault();
|
|
40
107
|
signatureRef.current.clear();
|
|
41
108
|
}
|
|
42
|
-
};
|
|
109
|
+
}, []);
|
|
43
110
|
|
|
44
|
-
const handleChangeProfile = (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
111
|
+
const handleChangeProfile = useCallback(
|
|
112
|
+
(e) => {
|
|
113
|
+
const { name, value } = e.target;
|
|
114
|
+
setProfile((prevProfile) => ({
|
|
115
|
+
...prevProfile,
|
|
116
|
+
[name]: value,
|
|
117
|
+
}));
|
|
51
118
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
119
|
+
// Clear error state when user starts typing
|
|
120
|
+
if (inputClasses[name]) {
|
|
121
|
+
setInputClasses((prev) => ({
|
|
122
|
+
...prev,
|
|
123
|
+
[name]: '',
|
|
124
|
+
}));
|
|
56
125
|
}
|
|
126
|
+
},
|
|
127
|
+
[inputClasses]
|
|
128
|
+
);
|
|
57
129
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
email: profile.email.trim() === '' ? styles.inputError : '',
|
|
63
|
-
});
|
|
130
|
+
// Validate form fields
|
|
131
|
+
const validateForm = useCallback(() => {
|
|
132
|
+
let isValid = true;
|
|
133
|
+
let newInputClasses = { ...inputClasses };
|
|
64
134
|
|
|
65
|
-
|
|
66
|
-
|
|
135
|
+
// Validate required fields
|
|
136
|
+
if (profile.name.trim() === '') {
|
|
137
|
+
newInputClasses.name = styles.inputError;
|
|
138
|
+
isValid = false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (profile.email.trim() === '') {
|
|
142
|
+
newInputClasses.email = styles.inputError;
|
|
143
|
+
isValid = false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Validate password and confirmation
|
|
147
|
+
if (
|
|
148
|
+
profile.password &&
|
|
149
|
+
profile.password !== profile.password_confirmation
|
|
150
|
+
) {
|
|
151
|
+
newInputClasses.password = styles.inputError;
|
|
152
|
+
newInputClasses.password_confirmation = styles.inputError;
|
|
153
|
+
isValid = false;
|
|
154
|
+
toast.error('Passwords do not match.');
|
|
155
|
+
return { isValid, newInputClasses };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!isValid) {
|
|
159
|
+
toast.error('Please fill in all the required fields.');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
setInputClasses(newInputClasses);
|
|
163
|
+
return { isValid, newInputClasses };
|
|
164
|
+
}, [profile, inputClasses]);
|
|
165
|
+
|
|
166
|
+
const handleSubmitProfile = useCallback(
|
|
167
|
+
async (e) => {
|
|
168
|
+
try {
|
|
169
|
+
if (e) {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { isValid } = validateForm();
|
|
174
|
+
if (!isValid) return;
|
|
175
|
+
|
|
176
|
+
setIsLoading(true);
|
|
177
|
+
const toastId = toast.loading('Updating profile...');
|
|
178
|
+
|
|
179
|
+
// Create a new payload object
|
|
180
|
+
let payload = {
|
|
181
|
+
name: profile.name.trim(),
|
|
182
|
+
email: profile.email.trim(),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Conditionally add password to payload if it has a value
|
|
186
|
+
if (profile.password.trim() !== '') {
|
|
187
|
+
payload.password = profile.password;
|
|
188
|
+
payload.password_confirmation =
|
|
189
|
+
profile.password_confirmation;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check if signature pad has content before adding to payload
|
|
193
|
+
if (!signatureRef.current.isEmpty()) {
|
|
194
|
+
payload.signature = signatureRef.current.toDataURL();
|
|
195
|
+
} else if (userProfile.signature) {
|
|
196
|
+
// Keep existing signature if no new one is provided
|
|
197
|
+
payload.signature = userProfile.signature;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const res = await CustomFetch(
|
|
201
|
+
`/ajax/users/${userProfile.id}`,
|
|
202
|
+
'PUT',
|
|
203
|
+
payload
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Extract the updated user data from the response
|
|
207
|
+
const updatedUserData = res.data.user
|
|
208
|
+
? res.data.user
|
|
209
|
+
: res.data;
|
|
210
|
+
|
|
211
|
+
// Make sure we have a valid name from the response
|
|
212
|
+
const updatedName = updatedUserData.name || userProfile.name;
|
|
213
|
+
|
|
214
|
+
// Update the parent component's userProfile state
|
|
215
|
+
setUserProfile((prevState) => ({
|
|
216
|
+
...prevState,
|
|
217
|
+
name: updatedName,
|
|
218
|
+
signature: updatedUserData.signature || prevState.signature,
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
// Update local profile state with the updated data and reset password fields
|
|
222
|
+
setProfile((prev) => ({
|
|
223
|
+
...prev,
|
|
224
|
+
name: updatedName, // Keep the updated name
|
|
225
|
+
email: prev.email, // Keep the email
|
|
226
|
+
password: '',
|
|
227
|
+
password_confirmation: '',
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
toast.update(toastId, {
|
|
231
|
+
render: 'Your profile has been successfully updated.',
|
|
232
|
+
type: 'success',
|
|
233
|
+
isLoading: false,
|
|
234
|
+
autoClose: 5000,
|
|
235
|
+
closeButton: true,
|
|
236
|
+
});
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.error('Error updating profile:', err);
|
|
239
|
+
toast.error('Failed to update profile. Please try again.');
|
|
240
|
+
} finally {
|
|
241
|
+
setIsLoading(false);
|
|
67
242
|
}
|
|
243
|
+
},
|
|
244
|
+
[
|
|
245
|
+
profile,
|
|
246
|
+
userProfile.id,
|
|
247
|
+
userProfile.name,
|
|
248
|
+
userProfile.signature,
|
|
249
|
+
validateForm,
|
|
250
|
+
setUserProfile,
|
|
251
|
+
]
|
|
252
|
+
);
|
|
68
253
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
254
|
+
// Function to fetch 2FA setup data (QR code and secret key)
|
|
255
|
+
const fetchTwoFactorSetupData = useCallback(async () => {
|
|
256
|
+
try {
|
|
257
|
+
setIsLoading(true);
|
|
258
|
+
const toastId = toast.loading('Fetching 2FA setup data...');
|
|
259
|
+
|
|
260
|
+
const res = await CustomFetch(
|
|
261
|
+
`/ajax/user/two-factor-auth/enable`,
|
|
262
|
+
'POST',
|
|
263
|
+
{}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (res.data) {
|
|
267
|
+
// Update state based on the Laravel response structure
|
|
268
|
+
setTwoFactorData({
|
|
269
|
+
qrCodeUrl: res.data.qr_code || '',
|
|
270
|
+
secretKey: res.data.secret_key || '', // Using the secret_key from the response
|
|
271
|
+
recoveryCodes: res.data.recovery_codes || [],
|
|
78
272
|
});
|
|
79
273
|
|
|
80
|
-
|
|
274
|
+
// Update 2FA status based on response
|
|
275
|
+
setTwoFactorEnabled(res.data.two_factor_enabled || false);
|
|
276
|
+
// Only show setup if not confirmed yet
|
|
277
|
+
if (!res.data.two_factor_confirmed) {
|
|
278
|
+
setShowTwoFactorSetup(true);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
toast.update(toastId, {
|
|
282
|
+
render: 'Two-factor authentication setup data fetched successfully',
|
|
283
|
+
type: 'success',
|
|
284
|
+
isLoading: false,
|
|
285
|
+
autoClose: 3000,
|
|
286
|
+
closeButton: true,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.error('Error fetching 2FA setup data:', err);
|
|
291
|
+
toast.error('Failed to fetch 2FA setup data. Please try again.');
|
|
292
|
+
} finally {
|
|
293
|
+
setIsLoading(false);
|
|
294
|
+
}
|
|
295
|
+
}, []);
|
|
296
|
+
|
|
297
|
+
// Function to enable 2FA with verification code
|
|
298
|
+
const enableTwoFactor = useCallback(async () => {
|
|
299
|
+
try {
|
|
300
|
+
if (!verificationCode.trim()) {
|
|
301
|
+
toast.error('Please enter the verification code');
|
|
81
302
|
return;
|
|
82
303
|
}
|
|
83
304
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
305
|
+
setIsLoading(true);
|
|
306
|
+
const toastId = toast.loading(
|
|
307
|
+
'Enabling two-factor authentication...'
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const res = await CustomFetch(
|
|
311
|
+
`/ajax/user/two-factor-auth/confirm`,
|
|
312
|
+
'POST',
|
|
313
|
+
{
|
|
314
|
+
code: verificationCode.trim(),
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (res.data && res.data.two_factor_confirmed) {
|
|
319
|
+
setTwoFactorEnabled(res.data.two_factor_enabled || true);
|
|
320
|
+
setShowTwoFactorSetup(false);
|
|
321
|
+
setVerificationCode('');
|
|
89
322
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
323
|
+
toast.update(toastId, {
|
|
324
|
+
render: 'Two-factor authentication enabled successfully',
|
|
325
|
+
type: 'success',
|
|
326
|
+
isLoading: false,
|
|
327
|
+
autoClose: 3000,
|
|
328
|
+
closeButton: true,
|
|
329
|
+
});
|
|
330
|
+
} else {
|
|
331
|
+
toast.update(toastId, {
|
|
332
|
+
render:
|
|
333
|
+
res.data.message ||
|
|
334
|
+
'Failed to confirm two-factor authentication. Please try again.',
|
|
335
|
+
type: 'error',
|
|
336
|
+
isLoading: false,
|
|
337
|
+
autoClose: 3000,
|
|
338
|
+
closeButton: true,
|
|
339
|
+
});
|
|
94
340
|
}
|
|
341
|
+
} catch (err) {
|
|
342
|
+
console.error('Error enabling 2FA:', err);
|
|
343
|
+
toast.error(
|
|
344
|
+
'Failed to enable two-factor authentication. Please try again.'
|
|
345
|
+
);
|
|
346
|
+
} finally {
|
|
347
|
+
setIsLoading(false);
|
|
348
|
+
}
|
|
349
|
+
}, [verificationCode]);
|
|
95
350
|
|
|
96
|
-
|
|
351
|
+
// Function to disable 2FA
|
|
352
|
+
const disableTwoFactor = useCallback(async () => {
|
|
353
|
+
try {
|
|
354
|
+
if (
|
|
355
|
+
!window.confirm(
|
|
356
|
+
'Are you sure you want to disable two-factor authentication? This will make your account less secure.'
|
|
357
|
+
)
|
|
358
|
+
) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
setIsLoading(true);
|
|
363
|
+
const toastId = toast.loading(
|
|
364
|
+
'Disabling two-factor authentication...'
|
|
365
|
+
);
|
|
97
366
|
|
|
98
367
|
const res = await CustomFetch(
|
|
99
|
-
`/ajax/
|
|
100
|
-
'
|
|
101
|
-
|
|
368
|
+
`/ajax/user/two-factor-auth/disable`,
|
|
369
|
+
'POST',
|
|
370
|
+
{}
|
|
102
371
|
);
|
|
103
372
|
|
|
104
|
-
|
|
373
|
+
if (res.data) {
|
|
374
|
+
setTwoFactorEnabled(res.data.two_factor_enabled || false);
|
|
105
375
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
376
|
+
toast.update(toastId, {
|
|
377
|
+
render: 'Two-factor authentication disabled successfully',
|
|
378
|
+
type: 'success',
|
|
379
|
+
isLoading: false,
|
|
380
|
+
autoClose: 3000,
|
|
381
|
+
closeButton: true,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
112
384
|
} catch (err) {
|
|
113
|
-
console.
|
|
385
|
+
console.error('Error disabling 2FA:', err);
|
|
386
|
+
toast.error(
|
|
387
|
+
'Failed to disable two-factor authentication. Please try again.'
|
|
388
|
+
);
|
|
389
|
+
} finally {
|
|
390
|
+
setIsLoading(false);
|
|
114
391
|
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
392
|
+
}, []);
|
|
393
|
+
|
|
394
|
+
// Function to copy text to clipboard
|
|
395
|
+
const copyToClipboard = useCallback((text) => {
|
|
396
|
+
navigator.clipboard
|
|
397
|
+
.writeText(text)
|
|
398
|
+
.then(() => toast.success('Copied to clipboard'))
|
|
399
|
+
.catch(() => toast.error('Failed to copy to clipboard'));
|
|
400
|
+
}, []);
|
|
401
|
+
|
|
402
|
+
// Handle verification code input change
|
|
403
|
+
const handleVerificationCodeChange = useCallback((e) => {
|
|
404
|
+
const value = e.target.value;
|
|
405
|
+
// Only allow digits and limit to 6 characters
|
|
406
|
+
if (/^\d*$/.test(value) && value.length <= 6) {
|
|
407
|
+
setVerificationCode(value);
|
|
408
|
+
}
|
|
409
|
+
}, []);
|
|
410
|
+
|
|
411
|
+
// Memoized content renderer to prevent unnecessary re-renders
|
|
412
|
+
const renderContent = useCallback(
|
|
413
|
+
(filter) => {
|
|
414
|
+
if (filter.show && filter.id === 'overview') {
|
|
415
|
+
return (
|
|
416
|
+
<div className={styles.gridtxt} key={filter.id}>
|
|
417
|
+
<div className={styles.gridtxt__header}>
|
|
418
|
+
<span>Overview</span>
|
|
419
|
+
</div>
|
|
420
|
+
<div className={styles.formcontainer}>
|
|
421
|
+
<form
|
|
422
|
+
className={styles.modalForm}
|
|
423
|
+
onSubmit={handleSubmitProfile}
|
|
424
|
+
>
|
|
425
|
+
<FormField
|
|
426
|
+
name="name"
|
|
427
|
+
value={profile.name}
|
|
428
|
+
onChange={handleChangeProfile}
|
|
429
|
+
label="Name *"
|
|
430
|
+
error={inputClasses.name}
|
|
431
|
+
/>
|
|
432
|
+
|
|
433
|
+
<FormField
|
|
434
|
+
name="email"
|
|
435
|
+
value={profile.email}
|
|
436
|
+
readOnly={true}
|
|
437
|
+
label="Email *"
|
|
438
|
+
error={inputClasses.email}
|
|
439
|
+
/>
|
|
440
|
+
|
|
441
|
+
<FormField
|
|
442
|
+
type="password"
|
|
443
|
+
name="password"
|
|
444
|
+
value={profile.password}
|
|
445
|
+
onChange={handleChangeProfile}
|
|
446
|
+
label="Password"
|
|
447
|
+
error={inputClasses.password}
|
|
448
|
+
>
|
|
171
449
|
<PasswordStrengthBar
|
|
172
450
|
password={profile.password}
|
|
173
451
|
/>
|
|
174
|
-
</
|
|
175
|
-
</div>
|
|
176
|
-
<div className={styles.formItem}>
|
|
177
|
-
<label className={styles.fi__label}>
|
|
178
|
-
<span
|
|
179
|
-
className={`${styles.fi__span} ${styles.prefocus}`}
|
|
180
|
-
>
|
|
181
|
-
Password Confirmation
|
|
182
|
-
</span>
|
|
183
|
-
<input
|
|
184
|
-
type="password"
|
|
185
|
-
name="password_confirmation"
|
|
186
|
-
onChange={handleChangeProfile}
|
|
187
|
-
value={profile.password_confirmation}
|
|
188
|
-
className={`${styles.input} ${inputClasses.password_confirmation}`}
|
|
189
|
-
/>
|
|
190
|
-
</label>
|
|
191
|
-
</div>
|
|
192
|
-
<div className={styles.formItem}>
|
|
193
|
-
<label className={styles.fi__label}>
|
|
194
|
-
<SignaturePad
|
|
195
|
-
ref={signatureRef}
|
|
196
|
-
height={200}
|
|
197
|
-
options={{
|
|
198
|
-
penColor: 'black',
|
|
199
|
-
}}
|
|
200
|
-
redrawOnResize
|
|
201
|
-
canvasProps={{
|
|
202
|
-
style: {
|
|
203
|
-
border: '2px solid #ccc',
|
|
204
|
-
borderRadius: '10px',
|
|
205
|
-
padding: '10px',
|
|
206
|
-
},
|
|
207
|
-
className: styles.sigcanvas,
|
|
208
|
-
}}
|
|
209
|
-
/>
|
|
210
|
-
<span className={styles.fi__span}>
|
|
211
|
-
Signature
|
|
212
|
-
</span>
|
|
213
|
-
</label>
|
|
214
|
-
</div>
|
|
215
|
-
</form>
|
|
216
|
-
</div>
|
|
452
|
+
</FormField>
|
|
217
453
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
454
|
+
<FormField
|
|
455
|
+
type="password"
|
|
456
|
+
name="password_confirmation"
|
|
457
|
+
value={profile.password_confirmation}
|
|
458
|
+
onChange={handleChangeProfile}
|
|
459
|
+
label="Password Confirmation"
|
|
460
|
+
error={inputClasses.password_confirmation}
|
|
461
|
+
/>
|
|
462
|
+
|
|
463
|
+
<div className={styles.formItem}>
|
|
464
|
+
<label className={styles.fi__label}>
|
|
465
|
+
<span
|
|
466
|
+
className={`${styles.fi__span} ${styles.prefocus}`}
|
|
467
|
+
>
|
|
468
|
+
Signature
|
|
469
|
+
</span>
|
|
470
|
+
<div
|
|
471
|
+
className={
|
|
472
|
+
styles.signaturePadWrapper
|
|
473
|
+
}
|
|
474
|
+
>
|
|
475
|
+
<SignaturePad
|
|
476
|
+
ref={signatureRef}
|
|
477
|
+
height={200}
|
|
478
|
+
options={{
|
|
479
|
+
penColor: 'black',
|
|
480
|
+
backgroundColor:
|
|
481
|
+
'rgba(255, 255, 255, 0.9)',
|
|
482
|
+
}}
|
|
483
|
+
redrawOnResize
|
|
484
|
+
canvasProps={{
|
|
485
|
+
style: {
|
|
486
|
+
border: '2px solid #ccc',
|
|
487
|
+
borderRadius: '10px',
|
|
488
|
+
padding: '10px',
|
|
489
|
+
width: '100%',
|
|
490
|
+
},
|
|
491
|
+
className: styles.sigcanvas,
|
|
492
|
+
}}
|
|
493
|
+
/>
|
|
494
|
+
<small
|
|
495
|
+
style={{
|
|
496
|
+
display: 'block',
|
|
497
|
+
marginTop: '5px',
|
|
498
|
+
color: '#666',
|
|
499
|
+
}}
|
|
500
|
+
>
|
|
501
|
+
Draw your signature above
|
|
502
|
+
</small>
|
|
503
|
+
</div>
|
|
504
|
+
</label>
|
|
505
|
+
</div>
|
|
506
|
+
</form>
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
<div className={styles.polActions}>
|
|
510
|
+
<button
|
|
511
|
+
className={`${styles.btn} ${
|
|
512
|
+
isLoading ? styles.loading : ''
|
|
513
|
+
}`}
|
|
514
|
+
onClick={handleSubmitProfile}
|
|
515
|
+
disabled={isLoading}
|
|
516
|
+
aria-busy={isLoading}
|
|
517
|
+
>
|
|
518
|
+
{isLoading ? 'Saving...' : 'Save'}
|
|
519
|
+
</button>
|
|
520
|
+
<button
|
|
521
|
+
className={styles.btn}
|
|
522
|
+
onClick={handleClearSignature}
|
|
523
|
+
disabled={isLoading}
|
|
524
|
+
type="button"
|
|
525
|
+
>
|
|
526
|
+
Clear Signature
|
|
527
|
+
</button>
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
);
|
|
531
|
+
} else if (filter.show && filter.id === '2fa') {
|
|
532
|
+
return (
|
|
533
|
+
<div className={styles.gridtxt} key={filter.id}>
|
|
534
|
+
<div className={styles.gridtxt__header}>
|
|
535
|
+
<span>Two-Factor Authentication</span>
|
|
536
|
+
</div>
|
|
537
|
+
<div
|
|
538
|
+
className={styles.formcontainer}
|
|
539
|
+
style={{ maxWidth: '100%' }}
|
|
228
540
|
>
|
|
229
|
-
|
|
230
|
-
|
|
541
|
+
<div className={styles.twoFactorSection}>
|
|
542
|
+
<div className={styles.twoFactorStatus}>
|
|
543
|
+
<h3>
|
|
544
|
+
Status:{' '}
|
|
545
|
+
{twoFactorEnabled ? (
|
|
546
|
+
<span
|
|
547
|
+
className={styles.statusEnabled}
|
|
548
|
+
>
|
|
549
|
+
Enabled
|
|
550
|
+
</span>
|
|
551
|
+
) : (
|
|
552
|
+
<span
|
|
553
|
+
className={
|
|
554
|
+
styles.statusDisabled
|
|
555
|
+
}
|
|
556
|
+
>
|
|
557
|
+
Disabled
|
|
558
|
+
</span>
|
|
559
|
+
)}
|
|
560
|
+
</h3>
|
|
561
|
+
<p>
|
|
562
|
+
Two-factor authentication adds an extra
|
|
563
|
+
layer of security to your account. When
|
|
564
|
+
enabled, you'll need to provide both
|
|
565
|
+
your password and a verification code
|
|
566
|
+
from your authenticator app when signing
|
|
567
|
+
in.
|
|
568
|
+
</p>
|
|
569
|
+
|
|
570
|
+
{!twoFactorEnabled &&
|
|
571
|
+
!showTwoFactorSetup && (
|
|
572
|
+
<button
|
|
573
|
+
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
574
|
+
onClick={
|
|
575
|
+
fetchTwoFactorSetupData
|
|
576
|
+
}
|
|
577
|
+
disabled={isLoading}
|
|
578
|
+
>
|
|
579
|
+
{isLoading
|
|
580
|
+
? 'Loading...'
|
|
581
|
+
: 'Set up two-factor authentication'}
|
|
582
|
+
</button>
|
|
583
|
+
)}
|
|
584
|
+
|
|
585
|
+
{twoFactorEnabled &&
|
|
586
|
+
userProfile.two_factor_confirmed_at && (
|
|
587
|
+
<div>
|
|
588
|
+
<p
|
|
589
|
+
className={
|
|
590
|
+
styles.confirmedMessage
|
|
591
|
+
}
|
|
592
|
+
>
|
|
593
|
+
Two-factor authentication is
|
|
594
|
+
set up and confirmed. Your
|
|
595
|
+
account is now more secure.
|
|
596
|
+
</p>
|
|
597
|
+
<button
|
|
598
|
+
className={`${styles.btn} ${styles.btnDanger}`}
|
|
599
|
+
onClick={disableTwoFactor}
|
|
600
|
+
disabled={isLoading}
|
|
601
|
+
>
|
|
602
|
+
{isLoading
|
|
603
|
+
? 'Disabling...'
|
|
604
|
+
: 'Disable two-factor authentication'}
|
|
605
|
+
</button>
|
|
606
|
+
</div>
|
|
607
|
+
)}
|
|
608
|
+
|
|
609
|
+
{twoFactorEnabled &&
|
|
610
|
+
!userProfile.two_factor_confirmed_at && (
|
|
611
|
+
<button
|
|
612
|
+
className={`${styles.btn} ${styles.btnDanger}`}
|
|
613
|
+
onClick={disableTwoFactor}
|
|
614
|
+
disabled={isLoading}
|
|
615
|
+
>
|
|
616
|
+
{isLoading
|
|
617
|
+
? 'Disabling...'
|
|
618
|
+
: 'Disable two-factor authentication'}
|
|
619
|
+
</button>
|
|
620
|
+
)}
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
{showTwoFactorSetup && (
|
|
624
|
+
<div className={styles.twoFactorSetup}>
|
|
625
|
+
<h3>
|
|
626
|
+
Set up two-factor authentication
|
|
627
|
+
</h3>
|
|
628
|
+
<ol className={styles.setupSteps}>
|
|
629
|
+
<li>
|
|
630
|
+
<p>
|
|
631
|
+
Install an authenticator app
|
|
632
|
+
on your device (if you don't
|
|
633
|
+
already have one):
|
|
634
|
+
</p>
|
|
635
|
+
<ul>
|
|
636
|
+
<li>
|
|
637
|
+
<a
|
|
638
|
+
href="https://support.google.com/accounts/answer/1066447"
|
|
639
|
+
target="_blank"
|
|
640
|
+
rel="noopener noreferrer"
|
|
641
|
+
>
|
|
642
|
+
Google Authenticator
|
|
643
|
+
</a>
|
|
644
|
+
</li>
|
|
645
|
+
<li>
|
|
646
|
+
<a
|
|
647
|
+
href="https://authy.com/download/"
|
|
648
|
+
target="_blank"
|
|
649
|
+
rel="noopener noreferrer"
|
|
650
|
+
>
|
|
651
|
+
Authy
|
|
652
|
+
</a>
|
|
653
|
+
</li>
|
|
654
|
+
<li>
|
|
655
|
+
<a
|
|
656
|
+
href="https://www.microsoft.com/en-us/security/mobile-authenticator-app"
|
|
657
|
+
target="_blank"
|
|
658
|
+
rel="noopener noreferrer"
|
|
659
|
+
>
|
|
660
|
+
Microsoft
|
|
661
|
+
Authenticator
|
|
662
|
+
</a>
|
|
663
|
+
</li>
|
|
664
|
+
<li>
|
|
665
|
+
Or any other
|
|
666
|
+
TOTP-compatible
|
|
667
|
+
authenticator app
|
|
668
|
+
</li>
|
|
669
|
+
</ul>
|
|
670
|
+
</li>
|
|
671
|
+
<li>
|
|
672
|
+
<p>
|
|
673
|
+
Scan this QR code with your
|
|
674
|
+
authenticator app:
|
|
675
|
+
</p>
|
|
676
|
+
<div
|
|
677
|
+
className={
|
|
678
|
+
styles.qrCodeContainer
|
|
679
|
+
}
|
|
680
|
+
>
|
|
681
|
+
{twoFactorData.qrCodeUrl ? (
|
|
682
|
+
<div
|
|
683
|
+
dangerouslySetInnerHTML={{
|
|
684
|
+
__html: twoFactorData.qrCodeUrl,
|
|
685
|
+
}}
|
|
686
|
+
className={
|
|
687
|
+
styles.qrCodeSvg
|
|
688
|
+
}
|
|
689
|
+
/>
|
|
690
|
+
) : (
|
|
691
|
+
<div
|
|
692
|
+
className={
|
|
693
|
+
styles.qrPlaceholder
|
|
694
|
+
}
|
|
695
|
+
>
|
|
696
|
+
QR Code loading...
|
|
697
|
+
</div>
|
|
698
|
+
)}
|
|
699
|
+
</div>
|
|
700
|
+
</li>
|
|
701
|
+
<li>
|
|
702
|
+
<p>
|
|
703
|
+
Or manually enter this
|
|
704
|
+
secret key in your
|
|
705
|
+
authenticator app:
|
|
706
|
+
</p>
|
|
707
|
+
<div
|
|
708
|
+
className={
|
|
709
|
+
styles.secretKeyContainer
|
|
710
|
+
}
|
|
711
|
+
>
|
|
712
|
+
<code
|
|
713
|
+
className={
|
|
714
|
+
styles.secretKey
|
|
715
|
+
}
|
|
716
|
+
>
|
|
717
|
+
{
|
|
718
|
+
twoFactorData.secretKey
|
|
719
|
+
}
|
|
720
|
+
</code>
|
|
721
|
+
<button
|
|
722
|
+
className={
|
|
723
|
+
styles.copyButton
|
|
724
|
+
}
|
|
725
|
+
onClick={() =>
|
|
726
|
+
copyToClipboard(
|
|
727
|
+
twoFactorData.secretKey
|
|
728
|
+
)
|
|
729
|
+
}
|
|
730
|
+
type="button"
|
|
731
|
+
>
|
|
732
|
+
Copy
|
|
733
|
+
</button>
|
|
734
|
+
</div>
|
|
735
|
+
</li>
|
|
736
|
+
<li>
|
|
737
|
+
<p>
|
|
738
|
+
Enter the 6-digit code from
|
|
739
|
+
your app:
|
|
740
|
+
</p>
|
|
741
|
+
<div
|
|
742
|
+
className={
|
|
743
|
+
styles.verificationCodeContainer
|
|
744
|
+
}
|
|
745
|
+
>
|
|
746
|
+
<input
|
|
747
|
+
type="text"
|
|
748
|
+
className={
|
|
749
|
+
styles.verificationCodeInput
|
|
750
|
+
}
|
|
751
|
+
value={verificationCode}
|
|
752
|
+
onChange={
|
|
753
|
+
handleVerificationCodeChange
|
|
754
|
+
}
|
|
755
|
+
placeholder="000000"
|
|
756
|
+
maxLength={6}
|
|
757
|
+
/>
|
|
758
|
+
<button
|
|
759
|
+
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
760
|
+
onClick={
|
|
761
|
+
enableTwoFactor
|
|
762
|
+
}
|
|
763
|
+
disabled={
|
|
764
|
+
isLoading ||
|
|
765
|
+
verificationCode.length !==
|
|
766
|
+
6
|
|
767
|
+
}
|
|
768
|
+
>
|
|
769
|
+
{isLoading
|
|
770
|
+
? 'Verifying...'
|
|
771
|
+
: 'Verify'}
|
|
772
|
+
</button>
|
|
773
|
+
</div>
|
|
774
|
+
</li>
|
|
775
|
+
<li>
|
|
776
|
+
<p>
|
|
777
|
+
Save these recovery codes in
|
|
778
|
+
a secure place. If you lose
|
|
779
|
+
access to your authenticator
|
|
780
|
+
app, you can use these codes
|
|
781
|
+
to regain access to your
|
|
782
|
+
account:
|
|
783
|
+
</p>
|
|
784
|
+
<div
|
|
785
|
+
className={
|
|
786
|
+
styles.recoveryCodes
|
|
787
|
+
}
|
|
788
|
+
>
|
|
789
|
+
{twoFactorData.recoveryCodes &&
|
|
790
|
+
twoFactorData.recoveryCodes
|
|
791
|
+
.length > 0 ? (
|
|
792
|
+
<>
|
|
793
|
+
<div
|
|
794
|
+
className={
|
|
795
|
+
styles.recoveryCodesContainer
|
|
796
|
+
}
|
|
797
|
+
>
|
|
798
|
+
{twoFactorData.recoveryCodes.map(
|
|
799
|
+
(
|
|
800
|
+
code,
|
|
801
|
+
index
|
|
802
|
+
) => (
|
|
803
|
+
<div
|
|
804
|
+
key={
|
|
805
|
+
index
|
|
806
|
+
}
|
|
807
|
+
className={
|
|
808
|
+
styles.recoveryCode
|
|
809
|
+
}
|
|
810
|
+
>
|
|
811
|
+
<code>
|
|
812
|
+
{
|
|
813
|
+
code
|
|
814
|
+
}
|
|
815
|
+
</code>
|
|
816
|
+
</div>
|
|
817
|
+
)
|
|
818
|
+
)}
|
|
819
|
+
</div>
|
|
820
|
+
<button
|
|
821
|
+
className={
|
|
822
|
+
styles.copyButton
|
|
823
|
+
}
|
|
824
|
+
onClick={() =>
|
|
825
|
+
copyToClipboard(
|
|
826
|
+
twoFactorData.recoveryCodes.join(
|
|
827
|
+
'\n'
|
|
828
|
+
)
|
|
829
|
+
)
|
|
830
|
+
}
|
|
831
|
+
type="button"
|
|
832
|
+
>
|
|
833
|
+
Copy All Codes
|
|
834
|
+
</button>
|
|
835
|
+
</>
|
|
836
|
+
) : (
|
|
837
|
+
<div
|
|
838
|
+
className={
|
|
839
|
+
styles.noRecoveryCodes
|
|
840
|
+
}
|
|
841
|
+
>
|
|
842
|
+
No recovery codes
|
|
843
|
+
available
|
|
844
|
+
</div>
|
|
845
|
+
)}
|
|
846
|
+
</div>
|
|
847
|
+
<p
|
|
848
|
+
className={
|
|
849
|
+
styles.recoveryCodesWarning
|
|
850
|
+
}
|
|
851
|
+
>
|
|
852
|
+
<strong>Warning:</strong>{' '}
|
|
853
|
+
These codes will only be
|
|
854
|
+
shown once. Store them
|
|
855
|
+
securely as they allow
|
|
856
|
+
access to your account if
|
|
857
|
+
you lose your authenticator
|
|
858
|
+
device.
|
|
859
|
+
</p>
|
|
860
|
+
</li>
|
|
861
|
+
</ol>
|
|
862
|
+
<div className={styles.setupActions}>
|
|
863
|
+
<button
|
|
864
|
+
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
865
|
+
onClick={() =>
|
|
866
|
+
setShowTwoFactorSetup(false)
|
|
867
|
+
}
|
|
868
|
+
type="button"
|
|
869
|
+
disabled={isLoading}
|
|
870
|
+
>
|
|
871
|
+
Cancel
|
|
872
|
+
</button>
|
|
873
|
+
</div>
|
|
874
|
+
</div>
|
|
875
|
+
)}
|
|
876
|
+
</div>
|
|
877
|
+
</div>
|
|
231
878
|
</div>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
return null;
|
|
882
|
+
},
|
|
883
|
+
[
|
|
884
|
+
profile,
|
|
885
|
+
inputClasses,
|
|
886
|
+
handleChangeProfile,
|
|
887
|
+
handleSubmitProfile,
|
|
888
|
+
handleClearSignature,
|
|
889
|
+
isLoading,
|
|
890
|
+
twoFactorEnabled,
|
|
891
|
+
showTwoFactorSetup,
|
|
892
|
+
twoFactorData,
|
|
893
|
+
verificationCode,
|
|
894
|
+
fetchTwoFactorSetupData,
|
|
895
|
+
enableTwoFactor,
|
|
896
|
+
disableTwoFactor,
|
|
897
|
+
copyToClipboard,
|
|
898
|
+
handleVerificationCodeChange,
|
|
899
|
+
]
|
|
900
|
+
);
|
|
237
901
|
|
|
238
902
|
useEffect(() => {
|
|
903
|
+
// Reset form when userProfile changes
|
|
239
904
|
setProfile((prevState) => ({
|
|
240
905
|
...prevState,
|
|
241
|
-
name: userProfile.name,
|
|
242
|
-
email: userProfile.email,
|
|
906
|
+
name: userProfile.name || '',
|
|
907
|
+
email: userProfile.email || '',
|
|
243
908
|
password: '',
|
|
244
909
|
password_confirmation: '',
|
|
245
910
|
}));
|
|
246
911
|
|
|
247
|
-
|
|
248
|
-
|
|
912
|
+
// Reset input classes
|
|
913
|
+
setInputClasses({
|
|
914
|
+
name: '',
|
|
915
|
+
email: '',
|
|
916
|
+
password: '',
|
|
917
|
+
password_confirmation: '',
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
// Load signature if available
|
|
921
|
+
if (userProfile.signature && signatureRef.current) {
|
|
922
|
+
try {
|
|
923
|
+
signatureRef.current.fromDataURL(userProfile.signature);
|
|
924
|
+
} catch (error) {
|
|
925
|
+
console.error('Error loading signature:', error);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Check if 2FA is enabled for the user from userProfile
|
|
930
|
+
if (userProfile.two_factor_confirmed_at) {
|
|
931
|
+
setTwoFactorEnabled(true);
|
|
932
|
+
} else {
|
|
933
|
+
setTwoFactorEnabled(false);
|
|
249
934
|
}
|
|
250
|
-
}, [userProfile]);
|
|
935
|
+
}, [userProfile.name, userProfile.email, userProfile.signature]);
|
|
251
936
|
|
|
252
937
|
return (
|
|
253
938
|
<div>
|
|
@@ -279,4 +964,4 @@ const Profile = ({ userProfile, setUserProfile }) => {
|
|
|
279
964
|
);
|
|
280
965
|
};
|
|
281
966
|
|
|
282
|
-
export default Profile;
|
|
967
|
+
export default memo(Profile);
|