@visns-studio/visns-components 5.4.10 → 5.4.12

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 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
@@ -81,7 +81,7 @@
81
81
  "react-dom": "^17.0.0 || ^18.0.0"
82
82
  },
83
83
  "name": "@visns-studio/visns-components",
84
- "version": "5.4.10",
84
+ "version": "5.4.12",
85
85
  "description": "Various packages to assist in the development of our Custom Applications.",
86
86
  "main": "src/index.js",
87
87
  "files": [
@@ -102,6 +102,7 @@ const loadData = async (
102
102
  ...(fv.whereHas?.relation && {
103
103
  whereHas: fv.whereHas.relation,
104
104
  }),
105
+ orKey: fv.orKey || null,
105
106
  };
106
107
 
107
108
  if (existingIndex !== -1) {
@@ -2737,6 +2738,9 @@ const DataGrid = forwardRef(
2737
2738
  key: column.filter.key
2738
2739
  ? column.filter.key
2739
2740
  : null,
2741
+ orKey: column.filter.orKey
2742
+ ? column.filter.orKey
2743
+ : null,
2740
2744
  name: filterName,
2741
2745
  operator: column.filter.operator,
2742
2746
  type: column.filter.type,
@@ -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
- const toastId = toast.loading(
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
- toast.error(
344
- 'Failed to enable two-factor authentication. Please try again.'
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
- if (
355
- !window.confirm(
356
- 'Are you sure you want to disable two-factor authentication? This will make your account less secure.'
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
- }, [userProfile.name, userProfile.email, userProfile.signature]);
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
  />
@@ -1,7 +1,14 @@
1
1
  import '../../styles/global.css';
2
2
 
3
- import React, { useState } from 'react';
4
- import { Routes, Route, Navigate } from 'react-router-dom';
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 = {