@visns-studio/visns-components 5.4.9 → 5.4.11
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 +26 -0
- package/package.json +4 -2
- package/src/components/cms/DropZone.jsx +1 -0
- package/src/components/crm/Form.jsx +82 -26
- package/src/components/crm/auth/Profile.jsx +68 -14
- package/src/components/crm/generic/GenericAuth.jsx +2 -0
- package/src/components/crm/generic/GenericDetail.jsx +179 -41
- package/src/components/crm/generic/GenericIndex.jsx +22 -0
- package/src/components/crm/generic/GenericMain.jsx +57 -2
package/README.md
CHANGED
|
@@ -102,6 +102,7 @@ The main component that handles authentication and routing.
|
|
|
102
102
|
navigation={navigationConfig} // Navigation configuration
|
|
103
103
|
routeConfig={routeConfig} // Route configuration
|
|
104
104
|
themeType="primary" // Theme type: 'primary', 'secondary', etc.
|
|
105
|
+
enforce2FA={false} // Whether to enforce 2FA (default: false)
|
|
105
106
|
/>
|
|
106
107
|
```
|
|
107
108
|
|
|
@@ -341,6 +342,31 @@ Toast notification component.
|
|
|
341
342
|
4. User enters the verification code
|
|
342
343
|
5. If the code is valid, the user is authenticated and redirected to the main application
|
|
343
344
|
|
|
345
|
+
### Enforcing 2FA
|
|
346
|
+
|
|
347
|
+
You can enforce 2FA for all users by setting the `enforce2FA` prop to `true` on the GenericAuth component:
|
|
348
|
+
|
|
349
|
+
```jsx
|
|
350
|
+
<GenericAuth
|
|
351
|
+
// ... other props
|
|
352
|
+
enforce2FA={true}
|
|
353
|
+
/>
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
When 2FA enforcement is enabled:
|
|
357
|
+
|
|
358
|
+
1. Users without 2FA enabled will see a non-dismissible dialog prompting them to set up 2FA
|
|
359
|
+
2. The dialog will redirect them to the profile page with the 2FA tab open when they click the button
|
|
360
|
+
3. Users cannot dismiss or skip this dialog, ensuring they set up 2FA
|
|
361
|
+
4. The dialog uses an error icon to indicate the requirement
|
|
362
|
+
5. The dialog will appear on every page load and route change until 2FA is set up
|
|
363
|
+
6. The dialog will not appear when the user is already on the profile page with the 2FA tab open
|
|
364
|
+
|
|
365
|
+
When 2FA enforcement is disabled (default):
|
|
366
|
+
|
|
367
|
+
1. No alert is shown to users without 2FA
|
|
368
|
+
2. Users can still enable 2FA voluntarily through their profile page
|
|
369
|
+
|
|
344
370
|
### API Integration for 2FA
|
|
345
371
|
|
|
346
372
|
#### Login Endpoint Response (when 2FA is required)
|
package/package.json
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"array-move": "^4.0.0",
|
|
16
16
|
"awesome-debounce-promise": "^2.1.0",
|
|
17
17
|
"axios": "^1.8.4",
|
|
18
|
+
"browser-image-compression": "^2.0.2",
|
|
18
19
|
"dayjs": "^1.11.13",
|
|
19
20
|
"fabric": "^6.6.2",
|
|
20
21
|
"file-saver": "^2.0.5",
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"reactjs-popup": "^2.0.6",
|
|
54
55
|
"style-loader": "^4.0.0",
|
|
55
56
|
"swapy": "^1.0.5",
|
|
57
|
+
"sweetalert2": "^11.17.2",
|
|
56
58
|
"truncate": "^3.0.0",
|
|
57
59
|
"uuid": "^10.0.0",
|
|
58
60
|
"validator": "^13.15.0",
|
|
@@ -79,7 +81,7 @@
|
|
|
79
81
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
80
82
|
},
|
|
81
83
|
"name": "@visns-studio/visns-components",
|
|
82
|
-
"version": "5.4.
|
|
84
|
+
"version": "5.4.11",
|
|
83
85
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
84
86
|
"main": "src/index.js",
|
|
85
87
|
"files": [
|
|
@@ -100,5 +102,5 @@
|
|
|
100
102
|
"url": "https://github.com/visnsstudio/visns-components/issues"
|
|
101
103
|
},
|
|
102
104
|
"homepage": "https://github.com/visnsstudio/visns-components#readme",
|
|
103
|
-
"packageManager": "yarn@4.
|
|
105
|
+
"packageManager": "yarn@4.7.0"
|
|
104
106
|
}
|
|
@@ -6,6 +6,7 @@ import { CircleX, File, TrashCan } from 'akar-icons';
|
|
|
6
6
|
import { toast } from 'react-toastify';
|
|
7
7
|
import { arrayMoveImmutable } from 'array-move';
|
|
8
8
|
import Popup from 'reactjs-popup';
|
|
9
|
+
import imageCompression from 'browser-image-compression';
|
|
9
10
|
|
|
10
11
|
import 'react-confirm-alert/src/react-confirm-alert.css';
|
|
11
12
|
|
|
@@ -11,6 +11,7 @@ import _ from 'lodash';
|
|
|
11
11
|
import ReactDataGrid from '@inovua/reactdatagrid-community';
|
|
12
12
|
import { CircleX } from 'akar-icons';
|
|
13
13
|
import { confirmAlert } from 'react-confirm-alert';
|
|
14
|
+
import imageCompression from 'browser-image-compression';
|
|
14
15
|
|
|
15
16
|
import '@inovua/reactdatagrid-community/index.css';
|
|
16
17
|
import 'react-confirm-alert/src/react-confirm-alert.css';
|
|
@@ -22,6 +23,49 @@ import Table from './DataGrid';
|
|
|
22
23
|
|
|
23
24
|
import styles from './styles/Form.module.scss';
|
|
24
25
|
|
|
26
|
+
// Helper function to check if a file is an image and optimize it if needed
|
|
27
|
+
const optimizeImageIfNeeded = async (file) => {
|
|
28
|
+
// Check if the file is an image
|
|
29
|
+
const isImage = file.type.startsWith('image/');
|
|
30
|
+
|
|
31
|
+
if (isImage) {
|
|
32
|
+
// Image compression options
|
|
33
|
+
const options = {
|
|
34
|
+
maxSizeMB: 1, // Max file size in MB
|
|
35
|
+
maxWidthOrHeight: 1920, // Max width or height in pixels
|
|
36
|
+
useWebWorker: true,
|
|
37
|
+
fileType: file.type,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Show optimization toast
|
|
42
|
+
const optimizingToastId = toast.info(`Optimizing ${file.name}...`, {
|
|
43
|
+
autoClose: false,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Compress the image
|
|
47
|
+
const compressedFile = await imageCompression(file, options);
|
|
48
|
+
|
|
49
|
+
// Update toast
|
|
50
|
+
toast.update(optimizingToastId, {
|
|
51
|
+
render: `${file.name} optimized successfully!`,
|
|
52
|
+
type: 'success',
|
|
53
|
+
autoClose: 1000,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Return the compressed file
|
|
57
|
+
return compressedFile;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('Error optimizing image:', error);
|
|
60
|
+
// If optimization fails, return the original file
|
|
61
|
+
return file;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// If not an image, return the original file
|
|
66
|
+
return file;
|
|
67
|
+
};
|
|
68
|
+
|
|
25
69
|
function Form({
|
|
26
70
|
ajaxSetting,
|
|
27
71
|
api,
|
|
@@ -701,6 +745,10 @@ function Form({
|
|
|
701
745
|
if (Array.isArray(formData[fileKey])) {
|
|
702
746
|
const uploadPromises = formData[fileKey].map(
|
|
703
747
|
async (file) => {
|
|
748
|
+
// Optimize image if it's an image file
|
|
749
|
+
const optimizedFile =
|
|
750
|
+
await optimizeImageIfNeeded(file);
|
|
751
|
+
|
|
704
752
|
const toastId = toast.info(
|
|
705
753
|
`Uploading ${file.name}`,
|
|
706
754
|
{
|
|
@@ -709,14 +757,19 @@ function Form({
|
|
|
709
757
|
}
|
|
710
758
|
);
|
|
711
759
|
|
|
712
|
-
const fileRes = await Vapor.store(
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
760
|
+
const fileRes = await Vapor.store(
|
|
761
|
+
optimizedFile,
|
|
762
|
+
{
|
|
763
|
+
progress: (progress) => {
|
|
764
|
+
setUploadProgress(
|
|
765
|
+
progress * 100
|
|
766
|
+
);
|
|
767
|
+
toast.update(toastId, {
|
|
768
|
+
progress,
|
|
769
|
+
});
|
|
770
|
+
},
|
|
771
|
+
}
|
|
772
|
+
);
|
|
720
773
|
|
|
721
774
|
toast.update(toastId, {
|
|
722
775
|
render: `${file.name} uploaded successfully!`,
|
|
@@ -729,7 +782,7 @@ function Form({
|
|
|
729
782
|
key: fileRes.key,
|
|
730
783
|
bucket: fileRes.bucket,
|
|
731
784
|
filename: file.name,
|
|
732
|
-
filesize:
|
|
785
|
+
filesize: optimizedFile.size,
|
|
733
786
|
extension: fileRes.extension,
|
|
734
787
|
file_relationship: fileKey,
|
|
735
788
|
fileable_field:
|
|
@@ -742,6 +795,11 @@ function Form({
|
|
|
742
795
|
uploadPromises
|
|
743
796
|
);
|
|
744
797
|
} else {
|
|
798
|
+
// Optimize image if it's an image file
|
|
799
|
+
const optimizedFile = await optimizeImageIfNeeded(
|
|
800
|
+
formData[fileKey]
|
|
801
|
+
);
|
|
802
|
+
|
|
745
803
|
const toastId = toast.info(
|
|
746
804
|
`Uploading ${formData[fileKey].name}`,
|
|
747
805
|
{
|
|
@@ -750,17 +808,14 @@ function Form({
|
|
|
750
808
|
}
|
|
751
809
|
);
|
|
752
810
|
|
|
753
|
-
const fileRes = await Vapor.store(
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
},
|
|
762
|
-
}
|
|
763
|
-
);
|
|
811
|
+
const fileRes = await Vapor.store(optimizedFile, {
|
|
812
|
+
progress: (progress) => {
|
|
813
|
+
setUploadProgress(progress * 100);
|
|
814
|
+
toast.update(toastId, {
|
|
815
|
+
progress,
|
|
816
|
+
});
|
|
817
|
+
},
|
|
818
|
+
});
|
|
764
819
|
|
|
765
820
|
toast.update(toastId, {
|
|
766
821
|
render: `${formData[fileKey].name} uploaded successfully!`,
|
|
@@ -772,7 +827,7 @@ function Form({
|
|
|
772
827
|
formData['key'] = fileRes.key;
|
|
773
828
|
formData['bucket'] = fileRes.bucket;
|
|
774
829
|
formData['filename'] = formData[fileKey].name;
|
|
775
|
-
formData['filesize'] =
|
|
830
|
+
formData['filesize'] = optimizedFile.size;
|
|
776
831
|
formData['extension'] = fileRes.extension;
|
|
777
832
|
formData['file_relationship'] = fileKey;
|
|
778
833
|
formData['fileable_field'] =
|
|
@@ -930,8 +985,9 @@ function Form({
|
|
|
930
985
|
method,
|
|
931
986
|
payload = {};
|
|
932
987
|
|
|
933
|
-
|
|
934
|
-
|
|
988
|
+
// Helper function to get dataId from ajaxSetting
|
|
989
|
+
const getDataId = () => {
|
|
990
|
+
if (ajaxSetting?.where) {
|
|
935
991
|
for (const item of ajaxSetting.where) {
|
|
936
992
|
if (
|
|
937
993
|
item.hasOwnProperty('urlParam') &&
|
|
@@ -940,9 +996,9 @@ function Form({
|
|
|
940
996
|
return item.value;
|
|
941
997
|
}
|
|
942
998
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
}
|
|
999
|
+
}
|
|
1000
|
+
return 0;
|
|
1001
|
+
};
|
|
946
1002
|
|
|
947
1003
|
if (
|
|
948
1004
|
formSettings.hasOwnProperty('json') &&
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import '../../styles/global.css';
|
|
3
3
|
|
|
4
4
|
import React, { useState, useEffect, useRef, useCallback, memo } from 'react';
|
|
5
|
+
import { useLocation } from 'react-router-dom';
|
|
5
6
|
import PasswordStrengthBar from 'react-password-strength-bar';
|
|
6
7
|
import { toast } from 'react-toastify';
|
|
7
8
|
import SignaturePad from 'react-signature-pad-wrapper';
|
|
9
|
+
import Swal from 'sweetalert2';
|
|
8
10
|
import CustomFetch from '../Fetch';
|
|
9
11
|
import TableFilter from '../TableFilter';
|
|
10
12
|
import styles from './styles/Profile.module.scss';
|
|
@@ -60,6 +62,7 @@ const FormField = memo(
|
|
|
60
62
|
);
|
|
61
63
|
|
|
62
64
|
const Profile = ({ userProfile, setUserProfile }) => {
|
|
65
|
+
const location = useLocation();
|
|
63
66
|
const signatureRef = useRef(null);
|
|
64
67
|
const [isLoading, setIsLoading] = useState(false);
|
|
65
68
|
const [profile, setProfile] = useState({
|
|
@@ -296,6 +299,7 @@ const Profile = ({ userProfile, setUserProfile }) => {
|
|
|
296
299
|
|
|
297
300
|
// Function to enable 2FA with verification code
|
|
298
301
|
const enableTwoFactor = useCallback(async () => {
|
|
302
|
+
let toastId;
|
|
299
303
|
try {
|
|
300
304
|
if (!verificationCode.trim()) {
|
|
301
305
|
toast.error('Please enter the verification code');
|
|
@@ -303,9 +307,7 @@ const Profile = ({ userProfile, setUserProfile }) => {
|
|
|
303
307
|
}
|
|
304
308
|
|
|
305
309
|
setIsLoading(true);
|
|
306
|
-
|
|
307
|
-
'Enabling two-factor authentication...'
|
|
308
|
-
);
|
|
310
|
+
toastId = toast.loading('Enabling two-factor authentication...');
|
|
309
311
|
|
|
310
312
|
const res = await CustomFetch(
|
|
311
313
|
`/ajax/user/two-factor-auth/confirm`,
|
|
@@ -320,6 +322,12 @@ const Profile = ({ userProfile, setUserProfile }) => {
|
|
|
320
322
|
setShowTwoFactorSetup(false);
|
|
321
323
|
setVerificationCode('');
|
|
322
324
|
|
|
325
|
+
// Update the parent component's userProfile state to reflect 2FA is now enabled
|
|
326
|
+
setUserProfile((prevState) => ({
|
|
327
|
+
...prevState,
|
|
328
|
+
two_factor_confirmed_at: new Date().toISOString(), // Add confirmation timestamp
|
|
329
|
+
}));
|
|
330
|
+
|
|
323
331
|
toast.update(toastId, {
|
|
324
332
|
render: 'Two-factor authentication enabled successfully',
|
|
325
333
|
type: 'success',
|
|
@@ -340,22 +348,42 @@ const Profile = ({ userProfile, setUserProfile }) => {
|
|
|
340
348
|
}
|
|
341
349
|
} catch (err) {
|
|
342
350
|
console.error('Error enabling 2FA:', err);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
)
|
|
351
|
+
|
|
352
|
+
// Update the loading toast if it exists, otherwise create a new error toast
|
|
353
|
+
if (toastId) {
|
|
354
|
+
toast.update(toastId, {
|
|
355
|
+
render: 'Failed to enable two-factor authentication. Please try again.',
|
|
356
|
+
type: 'error',
|
|
357
|
+
isLoading: false,
|
|
358
|
+
autoClose: 3000,
|
|
359
|
+
closeButton: true,
|
|
360
|
+
});
|
|
361
|
+
} else {
|
|
362
|
+
toast.error(
|
|
363
|
+
'Failed to enable two-factor authentication. Please try again.'
|
|
364
|
+
);
|
|
365
|
+
}
|
|
346
366
|
} finally {
|
|
347
367
|
setIsLoading(false);
|
|
348
368
|
}
|
|
349
|
-
}, [verificationCode]);
|
|
369
|
+
}, [verificationCode, setUserProfile]);
|
|
350
370
|
|
|
351
371
|
// Function to disable 2FA
|
|
352
372
|
const disableTwoFactor = useCallback(async () => {
|
|
353
373
|
try {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
374
|
+
// Use SweetAlert2 instead of window.confirm
|
|
375
|
+
const result = await Swal.fire({
|
|
376
|
+
title: 'Disable Two-Factor Authentication',
|
|
377
|
+
text: 'Are you sure you want to disable two-factor authentication? This will make your account less secure.',
|
|
378
|
+
icon: 'warning',
|
|
379
|
+
showCancelButton: true,
|
|
380
|
+
confirmButtonColor: 'var(--primary-color)',
|
|
381
|
+
cancelButtonColor: 'var(--secondary-color)',
|
|
382
|
+
confirmButtonText: 'Yes, disable it',
|
|
383
|
+
cancelButtonText: 'No, keep it enabled',
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
if (!result.isConfirmed) {
|
|
359
387
|
return;
|
|
360
388
|
}
|
|
361
389
|
|
|
@@ -373,6 +401,12 @@ const Profile = ({ userProfile, setUserProfile }) => {
|
|
|
373
401
|
if (res.data) {
|
|
374
402
|
setTwoFactorEnabled(res.data.two_factor_enabled || false);
|
|
375
403
|
|
|
404
|
+
// Update the parent component's userProfile state to reflect 2FA is now disabled
|
|
405
|
+
setUserProfile((prevState) => ({
|
|
406
|
+
...prevState,
|
|
407
|
+
two_factor_confirmed_at: null, // Remove confirmation timestamp
|
|
408
|
+
}));
|
|
409
|
+
|
|
376
410
|
toast.update(toastId, {
|
|
377
411
|
render: 'Two-factor authentication disabled successfully',
|
|
378
412
|
type: 'success',
|
|
@@ -389,7 +423,7 @@ const Profile = ({ userProfile, setUserProfile }) => {
|
|
|
389
423
|
} finally {
|
|
390
424
|
setIsLoading(false);
|
|
391
425
|
}
|
|
392
|
-
}, []);
|
|
426
|
+
}, [setUserProfile]);
|
|
393
427
|
|
|
394
428
|
// Function to copy text to clipboard
|
|
395
429
|
const copyToClipboard = useCallback((text) => {
|
|
@@ -932,7 +966,27 @@ const Profile = ({ userProfile, setUserProfile }) => {
|
|
|
932
966
|
} else {
|
|
933
967
|
setTwoFactorEnabled(false);
|
|
934
968
|
}
|
|
935
|
-
|
|
969
|
+
|
|
970
|
+
// Check URL query parameters for tab selection
|
|
971
|
+
const queryParams = new URLSearchParams(location.search);
|
|
972
|
+
const tabParam = queryParams.get('tab');
|
|
973
|
+
|
|
974
|
+
if (tabParam === '2fa') {
|
|
975
|
+
// Update filters to show 2FA tab
|
|
976
|
+
setFilters((prevFilters) =>
|
|
977
|
+
prevFilters.map((filter) => ({
|
|
978
|
+
...filter,
|
|
979
|
+
show: filter.id === '2fa',
|
|
980
|
+
class: filter.id === '2fa' ? 'subactive' : '',
|
|
981
|
+
}))
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
}, [
|
|
985
|
+
userProfile.name,
|
|
986
|
+
userProfile.email,
|
|
987
|
+
userProfile.signature,
|
|
988
|
+
location.search,
|
|
989
|
+
]);
|
|
936
990
|
|
|
937
991
|
return (
|
|
938
992
|
<div>
|
|
@@ -42,6 +42,7 @@ const GenericAuth = ({
|
|
|
42
42
|
navigation,
|
|
43
43
|
routeConfig,
|
|
44
44
|
themeType = 'primary',
|
|
45
|
+
enforce2FA = false, // Default to not enforcing 2FA
|
|
45
46
|
}) => {
|
|
46
47
|
const navigate = useNavigate();
|
|
47
48
|
const location = useLocation();
|
|
@@ -143,6 +144,7 @@ const GenericAuth = ({
|
|
|
143
144
|
setSystemAuth={setIsAuthenticated}
|
|
144
145
|
themeType={themeType}
|
|
145
146
|
userProfile={userProfile}
|
|
147
|
+
enforce2FA={enforce2FA}
|
|
146
148
|
/>
|
|
147
149
|
}
|
|
148
150
|
/>
|
|
@@ -14,6 +14,7 @@ import truncate from 'truncate';
|
|
|
14
14
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
|
15
15
|
import { saveAs } from 'file-saver';
|
|
16
16
|
import { toast } from 'react-toastify';
|
|
17
|
+
import imageCompression from 'browser-image-compression';
|
|
17
18
|
import {
|
|
18
19
|
CircleCheck,
|
|
19
20
|
Copy,
|
|
@@ -43,6 +44,49 @@ import styles from './styles/GenericDetail.module.scss';
|
|
|
43
44
|
const localizer = momentLocalizer(moment);
|
|
44
45
|
const DnDCalendar = withDragAndDrop(Calendar);
|
|
45
46
|
|
|
47
|
+
// Helper function to check if a file is an image and optimize it if needed
|
|
48
|
+
const optimizeImageIfNeeded = async (file) => {
|
|
49
|
+
// Check if the file is an image
|
|
50
|
+
const isImage = file.type.startsWith('image/');
|
|
51
|
+
|
|
52
|
+
if (isImage) {
|
|
53
|
+
// Image compression options
|
|
54
|
+
const options = {
|
|
55
|
+
maxSizeMB: 1, // Max file size in MB
|
|
56
|
+
maxWidthOrHeight: 1920, // Max width or height in pixels
|
|
57
|
+
useWebWorker: true,
|
|
58
|
+
fileType: file.type,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Show optimization toast
|
|
63
|
+
const optimizingToastId = toast.info(`Optimizing ${file.name}...`, {
|
|
64
|
+
autoClose: false,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Compress the image
|
|
68
|
+
const compressedFile = await imageCompression(file, options);
|
|
69
|
+
|
|
70
|
+
// Update toast
|
|
71
|
+
toast.update(optimizingToastId, {
|
|
72
|
+
render: `${file.name} optimized successfully!`,
|
|
73
|
+
type: 'success',
|
|
74
|
+
autoClose: 1000,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Return the compressed file
|
|
78
|
+
return compressedFile;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('Error optimizing image:', error);
|
|
81
|
+
// If optimization fails, return the original file
|
|
82
|
+
return file;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// If not an image, return the original file
|
|
87
|
+
return file;
|
|
88
|
+
};
|
|
89
|
+
|
|
46
90
|
function GenericDetail({
|
|
47
91
|
extraUrlParam,
|
|
48
92
|
layout = 'full',
|
|
@@ -1275,63 +1319,157 @@ function GenericDetail({
|
|
|
1275
1319
|
</div>
|
|
1276
1320
|
<Dropzone
|
|
1277
1321
|
onDrop={(acceptedFiles) => {
|
|
1278
|
-
const uploadFile = (file) => {
|
|
1279
|
-
|
|
1322
|
+
const uploadFile = async (file) => {
|
|
1323
|
+
// Optimize image if it's an image file
|
|
1324
|
+
const optimizedFile =
|
|
1325
|
+
await optimizeImageIfNeeded(
|
|
1326
|
+
file
|
|
1327
|
+
);
|
|
1328
|
+
|
|
1329
|
+
// Show upload toast
|
|
1330
|
+
const uploadToastId =
|
|
1331
|
+
toast.info(
|
|
1332
|
+
`Uploading ${file.name}...`,
|
|
1333
|
+
{
|
|
1334
|
+
autoClose: false,
|
|
1335
|
+
}
|
|
1336
|
+
);
|
|
1337
|
+
|
|
1338
|
+
Vapor.store(optimizedFile, {
|
|
1280
1339
|
progress: (progress) => {
|
|
1281
1340
|
setLoadingProgress(
|
|
1282
1341
|
progress * 100
|
|
1283
1342
|
);
|
|
1284
1343
|
},
|
|
1285
|
-
})
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
response.extension,
|
|
1299
|
-
file_relationship:
|
|
1300
|
-
activeTabConfig.file_relationship,
|
|
1301
|
-
};
|
|
1344
|
+
})
|
|
1345
|
+
.then((response) => {
|
|
1346
|
+
setLoadingProgress(0);
|
|
1347
|
+
|
|
1348
|
+
// Update upload toast
|
|
1349
|
+
toast.update(
|
|
1350
|
+
uploadToastId,
|
|
1351
|
+
{
|
|
1352
|
+
render: `Processing ${file.name}...`,
|
|
1353
|
+
type: 'info',
|
|
1354
|
+
autoClose: false,
|
|
1355
|
+
}
|
|
1356
|
+
);
|
|
1302
1357
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1358
|
+
const requestBody = {
|
|
1359
|
+
uuid: response.uuid,
|
|
1360
|
+
key: response.key,
|
|
1361
|
+
bucket: response.bucket,
|
|
1362
|
+
filename: file.name,
|
|
1363
|
+
file_name:
|
|
1364
|
+
file.name,
|
|
1365
|
+
filesize:
|
|
1366
|
+
optimizedFile.size,
|
|
1367
|
+
file_size:
|
|
1368
|
+
optimizedFile.size,
|
|
1369
|
+
extension:
|
|
1370
|
+
response.extension,
|
|
1371
|
+
file_extension:
|
|
1372
|
+
response.extension,
|
|
1373
|
+
file_relationship:
|
|
1374
|
+
activeTabConfig.file_relationship,
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1377
|
+
CustomFetch(
|
|
1378
|
+
`${activeTabConfig.url}/${routeParams[urlParam]}`,
|
|
1379
|
+
'PUT',
|
|
1380
|
+
requestBody,
|
|
1381
|
+
(res) => {
|
|
1382
|
+
// Check if there's an error in the response
|
|
1383
|
+
if (
|
|
1384
|
+
res.error &&
|
|
1385
|
+
res.error !==
|
|
1386
|
+
''
|
|
1387
|
+
) {
|
|
1388
|
+
toast.update(
|
|
1389
|
+
uploadToastId,
|
|
1390
|
+
{
|
|
1391
|
+
render: `Error: ${res.error}`,
|
|
1392
|
+
type: 'error',
|
|
1393
|
+
autoClose: 5000,
|
|
1394
|
+
}
|
|
1395
|
+
);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if (
|
|
1399
|
+
activeTabConfig.file_relationship &&
|
|
1316
1400
|
res.data[
|
|
1317
1401
|
activeTabConfig
|
|
1318
1402
|
.file_relationship
|
|
1319
1403
|
]
|
|
1320
|
-
);
|
|
1321
|
-
} else {
|
|
1322
|
-
if (
|
|
1323
|
-
res?.data
|
|
1324
|
-
?.files
|
|
1325
1404
|
) {
|
|
1326
1405
|
setFiles(
|
|
1406
|
+
res
|
|
1407
|
+
.data[
|
|
1408
|
+
activeTabConfig
|
|
1409
|
+
.file_relationship
|
|
1410
|
+
]
|
|
1411
|
+
);
|
|
1412
|
+
// Update the main data state with the new data
|
|
1413
|
+
setData(
|
|
1327
1414
|
res.data
|
|
1328
|
-
.files
|
|
1329
1415
|
);
|
|
1416
|
+
|
|
1417
|
+
// Update the upload toast
|
|
1418
|
+
toast.update(
|
|
1419
|
+
uploadToastId,
|
|
1420
|
+
{
|
|
1421
|
+
render: `${file.name} uploaded successfully!`,
|
|
1422
|
+
type: 'success',
|
|
1423
|
+
autoClose: 3000,
|
|
1424
|
+
}
|
|
1425
|
+
);
|
|
1426
|
+
} else {
|
|
1427
|
+
if (
|
|
1428
|
+
res
|
|
1429
|
+
?.data
|
|
1430
|
+
?.files
|
|
1431
|
+
) {
|
|
1432
|
+
setFiles(
|
|
1433
|
+
res
|
|
1434
|
+
.data
|
|
1435
|
+
.files
|
|
1436
|
+
);
|
|
1437
|
+
// Update the main data state with the new data
|
|
1438
|
+
setData(
|
|
1439
|
+
res.data
|
|
1440
|
+
);
|
|
1441
|
+
|
|
1442
|
+
// Update the upload toast
|
|
1443
|
+
toast.update(
|
|
1444
|
+
uploadToastId,
|
|
1445
|
+
{
|
|
1446
|
+
render: `${file.name} uploaded successfully!`,
|
|
1447
|
+
type: 'success',
|
|
1448
|
+
autoClose: 3000,
|
|
1449
|
+
}
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1330
1452
|
}
|
|
1331
1453
|
}
|
|
1332
|
-
|
|
1333
|
-
)
|
|
1334
|
-
|
|
1454
|
+
);
|
|
1455
|
+
})
|
|
1456
|
+
.catch((error) => {
|
|
1457
|
+
console.error(
|
|
1458
|
+
'Error uploading file:',
|
|
1459
|
+
error
|
|
1460
|
+
);
|
|
1461
|
+
toast.update(
|
|
1462
|
+
uploadToastId,
|
|
1463
|
+
{
|
|
1464
|
+
render: `Error uploading file: ${
|
|
1465
|
+
error.message ||
|
|
1466
|
+
'Unknown error'
|
|
1467
|
+
}`,
|
|
1468
|
+
type: 'error',
|
|
1469
|
+
autoClose: 5000,
|
|
1470
|
+
}
|
|
1471
|
+
);
|
|
1472
|
+
});
|
|
1335
1473
|
};
|
|
1336
1474
|
|
|
1337
1475
|
acceptedFiles.forEach((a) => {
|
|
@@ -206,6 +206,17 @@ function GenericIndex({
|
|
|
206
206
|
rowsSelectedRef.current = rowsSelected;
|
|
207
207
|
}, [rowsSelected]);
|
|
208
208
|
|
|
209
|
+
const handleCustomAction = async () => {
|
|
210
|
+
try {
|
|
211
|
+
const { data, method, message, url } = setting?.functions
|
|
212
|
+
?.customAction
|
|
213
|
+
? setting.functions.customAction
|
|
214
|
+
: config.form.functions.customAction;
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error(err);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
209
220
|
const handleCheckboxUpdate = useCallback(async () => {
|
|
210
221
|
try {
|
|
211
222
|
const { data, method, message, url } = setting?.functions
|
|
@@ -548,6 +559,17 @@ function GenericIndex({
|
|
|
548
559
|
</button>
|
|
549
560
|
)}
|
|
550
561
|
|
|
562
|
+
{setting.functions?.customAction && (
|
|
563
|
+
<button className={styles.btn} onClick={handleCustomAction}>
|
|
564
|
+
{setting.functions.customAction.label || 'Delete'}
|
|
565
|
+
</button>
|
|
566
|
+
)}
|
|
567
|
+
{config?.form?.functions?.customAction && (
|
|
568
|
+
<button className={styles.btn} onClick={handleCustomAction}>
|
|
569
|
+
{config.form.functions.customAction.label || 'Delete'}
|
|
570
|
+
</button>
|
|
571
|
+
)}
|
|
572
|
+
|
|
551
573
|
{setting.export?.label && (
|
|
552
574
|
<button className={styles.btn} onClick={handleExport}>
|
|
553
575
|
{setting.export.label}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import '../../styles/global.css';
|
|
2
2
|
|
|
3
|
-
import React, { useState } from 'react';
|
|
4
|
-
import {
|
|
3
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Routes,
|
|
6
|
+
Route,
|
|
7
|
+
Navigate,
|
|
8
|
+
useNavigate,
|
|
9
|
+
useLocation,
|
|
10
|
+
} from 'react-router-dom';
|
|
11
|
+
import Swal from 'sweetalert2';
|
|
5
12
|
|
|
6
13
|
import Call from '../Call';
|
|
7
14
|
import Loader from '../Loader';
|
|
@@ -19,7 +26,10 @@ const GenericMain = ({
|
|
|
19
26
|
setUserProfile,
|
|
20
27
|
themeType,
|
|
21
28
|
userProfile,
|
|
29
|
+
enforce2FA = false, // Default to not enforcing 2FA
|
|
22
30
|
}) => {
|
|
31
|
+
const navigate = useNavigate();
|
|
32
|
+
const location = useLocation();
|
|
23
33
|
const [incomingCallData, setIncomingCallData] = useState({});
|
|
24
34
|
|
|
25
35
|
// Function to extract route parameters
|
|
@@ -29,6 +39,51 @@ const GenericMain = ({
|
|
|
29
39
|
};
|
|
30
40
|
|
|
31
41
|
// Recursive function to render routes
|
|
42
|
+
// Function to show 2FA enforcement alert
|
|
43
|
+
const show2FAEnforcementAlert = useCallback(() => {
|
|
44
|
+
// Only show the alert if:
|
|
45
|
+
// 1. User is authenticated (userProfile exists and has an ID)
|
|
46
|
+
// 2. 2FA is not enabled (two_factor_confirmed_at is null or undefined)
|
|
47
|
+
// 3. enforce2FA is true (only show alert when 2FA is enforced)
|
|
48
|
+
// 4. Not already on the profile page with 2FA tab
|
|
49
|
+
const isOnProfilePage =
|
|
50
|
+
location.pathname === '/profile' &&
|
|
51
|
+
location.search.includes('tab=2fa');
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
enforce2FA && // Only show alert when 2FA is enforced
|
|
55
|
+
userProfile &&
|
|
56
|
+
userProfile.id &&
|
|
57
|
+
!userProfile.two_factor_confirmed_at &&
|
|
58
|
+
!isOnProfilePage // Don't show alert if already on profile page with 2FA tab
|
|
59
|
+
) {
|
|
60
|
+
// Configure SweetAlert2 options for enforced 2FA
|
|
61
|
+
const swalOptions = {
|
|
62
|
+
title: 'Two-Factor Authentication Required',
|
|
63
|
+
text: 'Two-factor authentication is required for this system. You must set up 2FA to continue.',
|
|
64
|
+
icon: 'error',
|
|
65
|
+
confirmButtonColor: 'var(--primary-color)',
|
|
66
|
+
confirmButtonText: 'Set up 2FA now',
|
|
67
|
+
allowOutsideClick: false,
|
|
68
|
+
allowEscapeKey: false,
|
|
69
|
+
allowEnterKey: false,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Show SweetAlert2 dialog
|
|
73
|
+
Swal.fire(swalOptions).then((result) => {
|
|
74
|
+
// Only navigate to profile page when the user clicks the button
|
|
75
|
+
if (result.isConfirmed) {
|
|
76
|
+
navigate('/profile?tab=2fa');
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}, [userProfile, navigate, enforce2FA, location.pathname, location.search]);
|
|
81
|
+
|
|
82
|
+
// Check if 2FA is enabled whenever the route changes
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
show2FAEnforcementAlert();
|
|
85
|
+
}, [show2FAEnforcementAlert, location]);
|
|
86
|
+
|
|
32
87
|
const renderRoutes = (routes) => {
|
|
33
88
|
return routes.map((route, index) => {
|
|
34
89
|
let elementProps = {
|