@visns-studio/visns-components 5.4.6 → 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 +66 -10
- package/src/components/crm/auth/Profile.jsx +861 -176
- package/src/components/crm/auth/TwoFactorAuth.jsx +347 -0
- 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 +65 -0
- package/src/components/crm/generic/GenericAuth.jsx +23 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import '../../styles/global.css';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { useNavigate, useLocation } from 'react-router-dom';
|
|
5
|
+
import Reveal from 'react-reveal/Reveal';
|
|
6
|
+
import { toast } from 'react-toastify';
|
|
7
|
+
import { LockOff } from 'akar-icons';
|
|
8
|
+
|
|
9
|
+
import CustomFetch from '../Fetch';
|
|
10
|
+
|
|
11
|
+
import styles from './styles/TwoFactorAuth.module.scss';
|
|
12
|
+
|
|
13
|
+
const TwoFactorAuth = ({ logo, setSystemAuth, setUserProfile }) => {
|
|
14
|
+
const navigate = useNavigate();
|
|
15
|
+
const location = useLocation();
|
|
16
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
17
|
+
|
|
18
|
+
// Get user data from location state
|
|
19
|
+
const userData = location.state?.userData || null;
|
|
20
|
+
const previousUrl = location.state?.previousUrl || '';
|
|
21
|
+
const remember = location.state?.remember || false;
|
|
22
|
+
|
|
23
|
+
// If no userData, redirect to login
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!userData) {
|
|
26
|
+
navigate('/login');
|
|
27
|
+
}
|
|
28
|
+
}, [userData, navigate]);
|
|
29
|
+
|
|
30
|
+
const [verificationCode, setVerificationCode] = useState('');
|
|
31
|
+
const [codeClass, setCodeClass] = useState('');
|
|
32
|
+
|
|
33
|
+
const handleChange = (e) => {
|
|
34
|
+
if (e) {
|
|
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
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleSubmit = (e) => {
|
|
45
|
+
if (e) {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
|
|
48
|
+
if (!verificationCode.trim() || verificationCode.length !== 6) {
|
|
49
|
+
setCodeClass('inputError');
|
|
50
|
+
toast.error('Please enter a valid 6-digit code');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setIsLoading(true);
|
|
55
|
+
toast.info('Verifying code...', { autoClose: 2000 });
|
|
56
|
+
|
|
57
|
+
CustomFetch(
|
|
58
|
+
'/login/two-factor-challenge',
|
|
59
|
+
'POST',
|
|
60
|
+
{
|
|
61
|
+
code: verificationCode,
|
|
62
|
+
previous_url: previousUrl,
|
|
63
|
+
remember: remember,
|
|
64
|
+
},
|
|
65
|
+
(result) => {
|
|
66
|
+
setIsLoading(false);
|
|
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
|
|
92
|
+
} else {
|
|
93
|
+
// Display the error message
|
|
94
|
+
toast.error(result.error);
|
|
95
|
+
}
|
|
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
|
+
);
|
|
109
|
+
setCodeClass('inputError');
|
|
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;
|
|
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);
|
|
143
|
+
},
|
|
144
|
+
(error) => {
|
|
145
|
+
setIsLoading(false);
|
|
146
|
+
setCodeClass('inputError');
|
|
147
|
+
|
|
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);
|
|
189
|
+
|
|
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
|
+
}
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<div className={styles.lcontainer}>
|
|
258
|
+
<div className={styles.lwrap}>
|
|
259
|
+
<div className={styles.logincontainer}>
|
|
260
|
+
<form onSubmit={handleSubmit}>
|
|
261
|
+
<Reveal effect="fadeInUp">
|
|
262
|
+
<div className={styles.login}>
|
|
263
|
+
<img
|
|
264
|
+
src={logo}
|
|
265
|
+
alt="CRM"
|
|
266
|
+
className={styles.loginlogo}
|
|
267
|
+
/>
|
|
268
|
+
<div
|
|
269
|
+
className={`${styles.formItem} ${styles.fwItem}`}
|
|
270
|
+
>
|
|
271
|
+
<h2>Two-Factor Authentication</h2>
|
|
272
|
+
<p className={styles.instructions}>
|
|
273
|
+
Please enter the 6-digit code from your
|
|
274
|
+
authenticator app.
|
|
275
|
+
</p>
|
|
276
|
+
</div>
|
|
277
|
+
<div
|
|
278
|
+
className={`${styles.formItem} ${styles.fwItem}`}
|
|
279
|
+
>
|
|
280
|
+
<label className={styles.fi__label}>
|
|
281
|
+
<input
|
|
282
|
+
type="text"
|
|
283
|
+
name="verificationCode"
|
|
284
|
+
value={verificationCode}
|
|
285
|
+
onChange={handleChange}
|
|
286
|
+
tabIndex="1"
|
|
287
|
+
className={codeClass}
|
|
288
|
+
placeholder=" "
|
|
289
|
+
autoComplete="one-time-code"
|
|
290
|
+
inputMode="numeric"
|
|
291
|
+
maxLength="6"
|
|
292
|
+
/>
|
|
293
|
+
<span className={styles.fi__span}>
|
|
294
|
+
Verification Code
|
|
295
|
+
</span>
|
|
296
|
+
<small>
|
|
297
|
+
<LockOff
|
|
298
|
+
strokeWidth={2}
|
|
299
|
+
size={18}
|
|
300
|
+
/>
|
|
301
|
+
</small>
|
|
302
|
+
</label>
|
|
303
|
+
</div>
|
|
304
|
+
<div
|
|
305
|
+
className={`${styles.formItem} ${styles.fwItem} ${styles.lastItem}`}
|
|
306
|
+
>
|
|
307
|
+
<button
|
|
308
|
+
className={styles.btn}
|
|
309
|
+
type="submit"
|
|
310
|
+
disabled={isLoading}
|
|
311
|
+
>
|
|
312
|
+
{isLoading ? 'Verifying...' : 'Verify'}
|
|
313
|
+
</button>
|
|
314
|
+
</div>
|
|
315
|
+
<div
|
|
316
|
+
className={`${styles.formItem} ${styles.fwItem} ${styles.lastItem} ${styles.forgotten}`}
|
|
317
|
+
>
|
|
318
|
+
<span>
|
|
319
|
+
Having trouble? Contact your
|
|
320
|
+
administrator for assistance.
|
|
321
|
+
</span>
|
|
322
|
+
</div>
|
|
323
|
+
<div
|
|
324
|
+
className={`${styles.formItem} ${styles.fwItem} ${styles.lastItem} ${styles.forgotten}`}
|
|
325
|
+
>
|
|
326
|
+
<span>
|
|
327
|
+
<a
|
|
328
|
+
href="#"
|
|
329
|
+
onClick={(e) => {
|
|
330
|
+
e.preventDefault();
|
|
331
|
+
navigate('/login');
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
334
|
+
Back to Login
|
|
335
|
+
</a>
|
|
336
|
+
</span>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</Reveal>
|
|
340
|
+
</form>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
export default TwoFactorAuth;
|
|
@@ -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;
|