@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
|
@@ -14,11 +14,12 @@ const TwoFactorAuth = ({ logo, setSystemAuth, setUserProfile }) => {
|
|
|
14
14
|
const navigate = useNavigate();
|
|
15
15
|
const location = useLocation();
|
|
16
16
|
const [isLoading, setIsLoading] = useState(false);
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
// Get user data from location state
|
|
19
19
|
const userData = location.state?.userData || null;
|
|
20
20
|
const previousUrl = location.state?.previousUrl || '';
|
|
21
|
-
|
|
21
|
+
const remember = location.state?.remember || false;
|
|
22
|
+
|
|
22
23
|
// If no userData, redirect to login
|
|
23
24
|
useEffect(() => {
|
|
24
25
|
if (!userData) {
|
|
@@ -31,82 +32,225 @@ const TwoFactorAuth = ({ logo, setSystemAuth, setUserProfile }) => {
|
|
|
31
32
|
|
|
32
33
|
const handleChange = (e) => {
|
|
33
34
|
if (e) {
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
const value = e.target.value;
|
|
36
|
+
// Only allow digits and limit to 6 characters
|
|
37
|
+
if (/^\d*$/.test(value) && value.length <= 6) {
|
|
38
|
+
setVerificationCode(value);
|
|
39
|
+
setCodeClass('');
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
};
|
|
38
43
|
|
|
39
44
|
const handleSubmit = (e) => {
|
|
40
45
|
if (e) {
|
|
41
46
|
e.preventDefault();
|
|
42
|
-
|
|
43
|
-
if (!verificationCode.trim()) {
|
|
47
|
+
|
|
48
|
+
if (!verificationCode.trim() || verificationCode.length !== 6) {
|
|
44
49
|
setCodeClass('inputError');
|
|
45
|
-
toast.error('Please enter
|
|
50
|
+
toast.error('Please enter a valid 6-digit code');
|
|
46
51
|
return;
|
|
47
52
|
}
|
|
48
|
-
|
|
53
|
+
|
|
49
54
|
setIsLoading(true);
|
|
50
55
|
toast.info('Verifying code...', { autoClose: 2000 });
|
|
51
56
|
|
|
52
57
|
CustomFetch(
|
|
53
|
-
'/login/
|
|
58
|
+
'/login/two-factor-challenge',
|
|
54
59
|
'POST',
|
|
55
60
|
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
code: verificationCode,
|
|
62
|
+
previous_url: previousUrl,
|
|
63
|
+
remember: remember,
|
|
59
64
|
},
|
|
60
65
|
(result) => {
|
|
61
66
|
setIsLoading(false);
|
|
62
|
-
if (result.error === '') {
|
|
63
|
-
toast.success('Two-factor authentication successful');
|
|
64
|
-
setUserProfile(result.user || userData);
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
// Log the result to help with debugging
|
|
69
|
+
console.log('2FA Result:', result);
|
|
70
|
+
|
|
71
|
+
// Check if the result contains an error message
|
|
72
|
+
if (result && result.error) {
|
|
73
|
+
setCodeClass('inputError');
|
|
74
|
+
console.log('Found error in result:', result.error);
|
|
75
|
+
|
|
76
|
+
// Check if it's a session timeout error
|
|
77
|
+
if (
|
|
78
|
+
result.error ===
|
|
79
|
+
'Invalid two-factor authentication session.'
|
|
80
|
+
) {
|
|
81
|
+
console.log(
|
|
82
|
+
'Session timeout detected, redirecting to login'
|
|
83
|
+
);
|
|
84
|
+
toast.error(
|
|
85
|
+
'Your session has expired. Please log in again.'
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Redirect to login page
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
navigate('/login');
|
|
91
|
+
}, 1500); // Short delay to allow the toast to be seen
|
|
68
92
|
} else {
|
|
69
|
-
|
|
70
|
-
|
|
93
|
+
// Display the error message
|
|
94
|
+
toast.error(result.error);
|
|
71
95
|
}
|
|
72
|
-
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Additional check for error in different format
|
|
100
|
+
if (
|
|
101
|
+
typeof result === 'string' &&
|
|
102
|
+
result.includes(
|
|
103
|
+
'Invalid two-factor authentication session'
|
|
104
|
+
)
|
|
105
|
+
) {
|
|
106
|
+
console.log(
|
|
107
|
+
'Session timeout detected in string result, redirecting to login'
|
|
108
|
+
);
|
|
73
109
|
setCodeClass('inputError');
|
|
74
|
-
toast.error(
|
|
110
|
+
toast.error(
|
|
111
|
+
'Your session has expired. Please log in again.'
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Redirect to login page
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
navigate('/login');
|
|
117
|
+
}, 1500);
|
|
118
|
+
return;
|
|
75
119
|
}
|
|
120
|
+
|
|
121
|
+
// If we get here, it means we received a 200 response with the user object
|
|
122
|
+
console.log(
|
|
123
|
+
'Authentication successful, updating user profile'
|
|
124
|
+
);
|
|
125
|
+
toast.success('Two-factor authentication successful');
|
|
126
|
+
|
|
127
|
+
// Update the user profile with the received data
|
|
128
|
+
setUserProfile(result);
|
|
129
|
+
|
|
130
|
+
// Set authentication state to true
|
|
131
|
+
setSystemAuth(true);
|
|
132
|
+
|
|
133
|
+
// Use a small delay to ensure state updates are processed
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
// Navigate based on previous URL if available
|
|
136
|
+
if (previousUrl && previousUrl !== '') {
|
|
137
|
+
window.location.href = previousUrl;
|
|
138
|
+
} else {
|
|
139
|
+
// Use window.location for a full page refresh to ensure session is recognized
|
|
140
|
+
window.location.href = '/';
|
|
141
|
+
}
|
|
142
|
+
}, 500);
|
|
76
143
|
},
|
|
77
144
|
(error) => {
|
|
78
145
|
setIsLoading(false);
|
|
79
146
|
setCodeClass('inputError');
|
|
80
|
-
toast.error(String(error) || 'Failed to verify code');
|
|
81
|
-
}
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
147
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
148
|
+
// Log the error to help with debugging
|
|
149
|
+
console.log('2FA Error:', error);
|
|
150
|
+
|
|
151
|
+
// Log more detailed information about the error response
|
|
152
|
+
if (error.response) {
|
|
153
|
+
console.log('Error Response:', error.response);
|
|
154
|
+
console.log(
|
|
155
|
+
'Error Response Data:',
|
|
156
|
+
error.response.data
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Extract the error message from the response
|
|
161
|
+
let errorMessage =
|
|
162
|
+
'Invalid verification code. Please try again.';
|
|
163
|
+
|
|
164
|
+
// Direct check for the specific error we're looking for
|
|
165
|
+
// This is a temporary solution to force the redirect
|
|
166
|
+
if (
|
|
167
|
+
error &&
|
|
168
|
+
error.message === 'Request failed with status code 401'
|
|
169
|
+
) {
|
|
170
|
+
// Force redirect to login for 401 errors
|
|
171
|
+
console.log('Detected 401 error, redirecting to login');
|
|
172
|
+
toast.error(
|
|
173
|
+
'Your session has expired. Please log in again.'
|
|
174
|
+
);
|
|
175
|
+
setTimeout(() => {
|
|
176
|
+
navigate('/login');
|
|
177
|
+
}, 1500);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
// Try to parse the error response
|
|
183
|
+
if (error.response && error.response.data) {
|
|
184
|
+
console.log(
|
|
185
|
+
'Response data type:',
|
|
186
|
+
typeof error.response.data
|
|
187
|
+
);
|
|
188
|
+
console.log('Response data:', error.response.data);
|
|
92
189
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
190
|
+
// If data is a string, use it directly
|
|
191
|
+
if (typeof error.response.data === 'string') {
|
|
192
|
+
errorMessage = error.response.data;
|
|
193
|
+
}
|
|
194
|
+
// If data is an object with an error property
|
|
195
|
+
else if (typeof error.response.data === 'object') {
|
|
196
|
+
if (error.response.data.error) {
|
|
197
|
+
errorMessage = error.response.data.error;
|
|
198
|
+
console.log(
|
|
199
|
+
'Found error in object:',
|
|
200
|
+
errorMessage
|
|
201
|
+
);
|
|
202
|
+
} else if (error.response.data.message) {
|
|
203
|
+
errorMessage = error.response.data.message;
|
|
204
|
+
console.log(
|
|
205
|
+
'Found message in object:',
|
|
206
|
+
errorMessage
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// If data is JSON string, try to parse it
|
|
211
|
+
else if (
|
|
212
|
+
typeof error.response.data === 'string' &&
|
|
213
|
+
error.response.data.includes('{')
|
|
214
|
+
) {
|
|
215
|
+
const parsedData = JSON.parse(
|
|
216
|
+
error.response.data
|
|
217
|
+
);
|
|
218
|
+
if (parsedData.error) {
|
|
219
|
+
errorMessage = parsedData.error;
|
|
220
|
+
console.log(
|
|
221
|
+
'Found error in parsed JSON:',
|
|
222
|
+
errorMessage
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch (e) {
|
|
228
|
+
console.error('Error parsing error response:', e);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.log('Extracted error message:', errorMessage);
|
|
232
|
+
|
|
233
|
+
// Check if it's a session timeout error
|
|
234
|
+
if (
|
|
235
|
+
errorMessage ===
|
|
236
|
+
'Invalid two-factor authentication session.' ||
|
|
237
|
+
errorMessage.includes(
|
|
238
|
+
'Invalid two-factor authentication session'
|
|
239
|
+
)
|
|
240
|
+
) {
|
|
241
|
+
toast.error(
|
|
242
|
+
'Your session has expired. Please log in again.'
|
|
243
|
+
);
|
|
244
|
+
// Redirect to login page
|
|
245
|
+
setTimeout(() => {
|
|
246
|
+
navigate('/login');
|
|
247
|
+
}, 1500); // Short delay to allow the toast to be seen
|
|
248
|
+
} else {
|
|
249
|
+
toast.error(errorMessage);
|
|
250
|
+
}
|
|
104
251
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
toast.error(String(error) || 'Failed to send new code');
|
|
108
|
-
}
|
|
109
|
-
);
|
|
252
|
+
);
|
|
253
|
+
}
|
|
110
254
|
};
|
|
111
255
|
|
|
112
256
|
return (
|
|
@@ -121,13 +265,18 @@ const TwoFactorAuth = ({ logo, setSystemAuth, setUserProfile }) => {
|
|
|
121
265
|
alt="CRM"
|
|
122
266
|
className={styles.loginlogo}
|
|
123
267
|
/>
|
|
124
|
-
<div
|
|
268
|
+
<div
|
|
269
|
+
className={`${styles.formItem} ${styles.fwItem}`}
|
|
270
|
+
>
|
|
125
271
|
<h2>Two-Factor Authentication</h2>
|
|
126
272
|
<p className={styles.instructions}>
|
|
127
|
-
Please enter the
|
|
273
|
+
Please enter the 6-digit code from your
|
|
274
|
+
authenticator app.
|
|
128
275
|
</p>
|
|
129
276
|
</div>
|
|
130
|
-
<div
|
|
277
|
+
<div
|
|
278
|
+
className={`${styles.formItem} ${styles.fwItem}`}
|
|
279
|
+
>
|
|
131
280
|
<label className={styles.fi__label}>
|
|
132
281
|
<input
|
|
133
282
|
type="text"
|
|
@@ -138,17 +287,23 @@ const TwoFactorAuth = ({ logo, setSystemAuth, setUserProfile }) => {
|
|
|
138
287
|
className={codeClass}
|
|
139
288
|
placeholder=" "
|
|
140
289
|
autoComplete="one-time-code"
|
|
290
|
+
inputMode="numeric"
|
|
141
291
|
maxLength="6"
|
|
142
292
|
/>
|
|
143
293
|
<span className={styles.fi__span}>
|
|
144
294
|
Verification Code
|
|
145
295
|
</span>
|
|
146
296
|
<small>
|
|
147
|
-
<LockOff
|
|
297
|
+
<LockOff
|
|
298
|
+
strokeWidth={2}
|
|
299
|
+
size={18}
|
|
300
|
+
/>
|
|
148
301
|
</small>
|
|
149
302
|
</label>
|
|
150
303
|
</div>
|
|
151
|
-
<div
|
|
304
|
+
<div
|
|
305
|
+
className={`${styles.formItem} ${styles.fwItem} ${styles.lastItem}`}
|
|
306
|
+
>
|
|
152
307
|
<button
|
|
153
308
|
className={styles.btn}
|
|
154
309
|
type="submit"
|
|
@@ -157,17 +312,25 @@ const TwoFactorAuth = ({ logo, setSystemAuth, setUserProfile }) => {
|
|
|
157
312
|
{isLoading ? 'Verifying...' : 'Verify'}
|
|
158
313
|
</button>
|
|
159
314
|
</div>
|
|
160
|
-
<div
|
|
315
|
+
<div
|
|
316
|
+
className={`${styles.formItem} ${styles.fwItem} ${styles.lastItem} ${styles.forgotten}`}
|
|
317
|
+
>
|
|
161
318
|
<span>
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
Resend Code
|
|
165
|
-
</a>
|
|
319
|
+
Having trouble? Contact your
|
|
320
|
+
administrator for assistance.
|
|
166
321
|
</span>
|
|
167
322
|
</div>
|
|
168
|
-
<div
|
|
323
|
+
<div
|
|
324
|
+
className={`${styles.formItem} ${styles.fwItem} ${styles.lastItem} ${styles.forgotten}`}
|
|
325
|
+
>
|
|
169
326
|
<span>
|
|
170
|
-
<a
|
|
327
|
+
<a
|
|
328
|
+
href="#"
|
|
329
|
+
onClick={(e) => {
|
|
330
|
+
e.preventDefault();
|
|
331
|
+
navigate('/login');
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
171
334
|
Back to Login
|
|
172
335
|
</a>
|
|
173
336
|
</span>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
width: 50%;
|
|
3
3
|
position: relative;
|
|
4
4
|
box-sizing: border-box;
|
|
5
|
+
margin-bottom: 0.5rem;
|
|
5
6
|
|
|
6
7
|
.fi__label {
|
|
7
8
|
position: relative;
|
|
@@ -118,6 +119,41 @@
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
.rememberMeContainer {
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
justify-content: flex-start;
|
|
126
|
+
width: 100%;
|
|
127
|
+
padding: 0.5rem 1rem;
|
|
128
|
+
margin-bottom: 0.75rem;
|
|
129
|
+
box-sizing: border-box;
|
|
130
|
+
margin-top: -0.25rem;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.rememberMeCheckboxWrapper {
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
margin-right: 0.5rem;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.rememberMeLabel {
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
user-select: none;
|
|
143
|
+
color: var(--paragraph-color);
|
|
144
|
+
font-size: 0.85rem;
|
|
145
|
+
white-space: nowrap;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.rememberMeCheckbox {
|
|
149
|
+
/* Use the browser's default checkbox but with some styling */
|
|
150
|
+
width: 16px;
|
|
151
|
+
height: 16px;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
position: relative;
|
|
154
|
+
accent-color: var(--primary-color); /* Modern browsers support this */
|
|
155
|
+
}
|
|
156
|
+
|
|
121
157
|
.lcontainer {
|
|
122
158
|
min-height: 100vh;
|
|
123
159
|
display: flex;
|