Skip to content

Commit 310a091

Browse files
authored
Mfancher/prev passwords front (#1017)
* install bcrypt on front-end * store prev passwords on backend * Check for prev passwords on backend * Update authUtil.js * finish my account pw change * Dont require old password check on register form/wizard * Finalize password reset screens * Fixes * fix on page load * Bug Fix for reset temp password screen * Bug Fix on reset password with token
1 parent d9c1397 commit 310a091

File tree

8 files changed

+297
-52
lines changed

8 files changed

+297
-52
lines changed

Tombolo/client-reactjs/src/components/application/myAccount/changePasswordModal.jsx

+49-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React, { useState, useEffect, useRef } from 'react';
22
import { Modal, Form, Input, Button, Popover, message, Spin } from 'antd';
33
import passwordComplexityValidator from '../../common/passwordComplexityValidator';
44
import { changeBasicUserPassword } from './utils';
@@ -9,6 +9,40 @@ const ChangePasswordModal = ({ changePasswordModalVisible, setChangePasswordModa
99
const [popOverContent, setPopOverContent] = useState(null);
1010
const [loading, setLoading] = useState(false);
1111

12+
//ref to track if user is finished typing
13+
const finishedTypingRef = useRef(false);
14+
const isFirstLoad = useRef(true);
15+
16+
//need to detect when user is finished typing to run check previous password validator, otherwise perofrmance is too slow
17+
useEffect(() => {
18+
const timer = setTimeout(() => {
19+
if (!isFirstLoad.current) {
20+
validatePassword(form.getFieldValue('newPassword'), true);
21+
finishedTypingRef.current = true;
22+
form.validateFields(['newPassword']);
23+
} else {
24+
isFirstLoad.current = false;
25+
}
26+
}, 1000);
27+
28+
return () => clearTimeout(timer);
29+
}, [form.getFieldValue('newPassword')]);
30+
31+
const validatePassword = (value, checkOldPassword) => {
32+
let pw = value;
33+
if (!value) {
34+
pw = '';
35+
}
36+
37+
if (checkOldPassword) {
38+
setPopOverContent(
39+
passwordComplexityValidator({ password: pw, generateContent: true, user, oldPasswordCheck: true })
40+
);
41+
} else {
42+
setPopOverContent(passwordComplexityValidator({ password: pw, generateContent: true, user }));
43+
}
44+
};
45+
1246
const user = getUser();
1347

1448
const handleOk = async () => {
@@ -41,10 +75,6 @@ const ChangePasswordModal = ({ changePasswordModalVisible, setChangePasswordModa
4175

4276
useEffect(() => {}, [popOverContent]);
4377

44-
const validatePassword = (value) => {
45-
setPopOverContent(passwordComplexityValidator({ password: value, generateContent: true, user }));
46-
};
47-
4878
return (
4979
<Modal
5080
title="Change Password"
@@ -89,8 +119,17 @@ const ChangePasswordModal = ({ changePasswordModalVisible, setChangePasswordModa
89119
if (form.getFieldValue('currentPassword') === value) {
90120
return Promise.reject(new Error('New password cannot be the same as the current password!'));
91121
}
122+
let errors = [];
123+
124+
if (finishedTypingRef.current) {
125+
errors = passwordComplexityValidator({ password: value, user, oldPasswordCheck: true });
126+
} else {
127+
errors = passwordComplexityValidator({ password: value, user });
128+
}
129+
130+
finishedTypingRef.current = false;
131+
92132
//passwordComplexityValidator always returns an array with at least one attributes element
93-
const errors = passwordComplexityValidator({ password: value, user });
94133
if (!value || errors.length === 1) {
95134
return Promise.resolve();
96135
} else {
@@ -106,7 +145,10 @@ const ChangePasswordModal = ({ changePasswordModalVisible, setChangePasswordModa
106145
validatePassword(e.target.value);
107146
}}
108147
onFocus={(e) => {
109-
validatePassword(e.target.value);
148+
validatePassword(e.target.value, true);
149+
}}
150+
onBlur={(e) => {
151+
validatePassword(e.target.value, true);
110152
}}
111153
/>
112154
</Form.Item>

Tombolo/client-reactjs/src/components/common/passwordComplexityValidator.js

+36-4
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,37 @@
11
import React from 'react';
2-
import { CloseCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
2+
import { CloseCircleOutlined, CheckCircleOutlined, LoadingOutlined } from '@ant-design/icons';
3+
import bcrypt from 'bcryptjs-react';
34

4-
function passwordComplexityValidator({ password, generateContent, user }) {
5+
function passwordComplexityValidator({ password, generateContent, user, oldPasswordCheck, newUser }) {
56
// Define your password complexity rules here
67
const minLength = 8;
78
const hasUppercase = /[A-Z]/.test(password);
89
const hasLowercase = /[a-z]/.test(password);
910
const hasNumber = /[0-9]/.test(password);
1011
const hasSpecialChar = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password);
12+
1113
const isNotUserInfo =
1214
!password.trim().toLowerCase().includes(user?.firstName.trim().toLowerCase()) &&
1315
!password.includes(user?.lastName.trim().toLowerCase()) &&
1416
!password.includes(user?.email.trim().toLowerCase());
1517

18+
//need to only check for old passwords if oldPasswordCheck flag is passed,
19+
//this is to avoid performance issues when checking password complexity on the client side
20+
let isNotOldPassword = 'loading';
21+
if (oldPasswordCheck && !newUser) {
22+
isNotOldPassword = user?.metaData?.previousPasswords?.every((oldPassword) => {
23+
return !bcrypt.compareSync(password, oldPassword);
24+
});
25+
}
26+
1627
// Define your error messages here
1728
const uppercaseMessage = 'Password must contain at least one uppercase letter';
1829
const lowercaseMessage = 'Password must contain at least one lowercase letter';
1930
const numberMessage = 'Password must contain at least one number';
2031
const specialMessage = 'Password must contain at least one special character';
2132
const lengthMessage = `Password must be at least ${minLength} characters long`;
2233
const userInfoMessage = 'Password cannot contain your name or email address';
34+
const oldPasswordMessage = 'Password cannot be the same as old passwords';
2335

2436
let errors = [];
2537
errors.push({
@@ -33,6 +45,10 @@ function passwordComplexityValidator({ password, generateContent, user }) {
3345
],
3446
});
3547

48+
if (!newUser) {
49+
errors[0].attributes.push({ name: 'oldPassword', message: oldPasswordMessage });
50+
}
51+
3652
//checks if password meets the requirements
3753
if (!hasUppercase) {
3854
errors.push({ type: 'uppercase' });
@@ -52,16 +68,32 @@ function passwordComplexityValidator({ password, generateContent, user }) {
5268
if (!isNotUserInfo) {
5369
errors.push({ type: 'userInfo' });
5470
}
71+
if (!isNotOldPassword && oldPasswordCheck && !newUser) {
72+
errors.push({ type: 'oldPassword' });
73+
}
5574

5675
if (generateContent) {
5776
const passwordComplexityContent = errors[0].attributes.map((error) => {
5877
const errorExistsForAttribute = errors.some((error2) => error2?.type === error.name);
78+
5979
return (
6080
<li key={error.name} style={{ marginBottom: '.5rem' }}>
6181
{errorExistsForAttribute ? (
62-
<CloseCircleOutlined style={{ color: 'red', marginRight: '.5rem' }} />
82+
<>
83+
{error.name === 'oldPassword' && isNotOldPassword === 'loading' ? (
84+
<LoadingOutlined style={{ color: 'orange', marginRight: '.5rem' }} />
85+
) : (
86+
<CloseCircleOutlined style={{ color: 'red', marginRight: '.5rem' }} />
87+
)}
88+
</>
6389
) : (
64-
<CheckCircleOutlined style={{ color: 'green', marginRight: '.5rem' }} />
90+
<>
91+
{error.name === 'oldPassword' && isNotOldPassword === 'loading' ? (
92+
<LoadingOutlined style={{ color: 'orange', marginRight: '.5rem' }} />
93+
) : (
94+
<CheckCircleOutlined style={{ color: 'green', marginRight: '.5rem' }} />
95+
)}
96+
</>
6597
)}
6698
<span>{error.message}</span>
6799
</li>

Tombolo/client-reactjs/src/components/login/ResetPasswordWithToken.js

+65-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from 'react';
1+
import React, { useEffect, useState, useRef } from 'react';
22
import { Form, Input, Button, Divider, message, Popover } from 'antd';
33
import { useParams } from 'react-router-dom';
44
import passwordComplexityValidator from '../common/passwordComplexityValidator';
@@ -17,6 +17,47 @@ const ResetPassword = () => {
1717

1818
const [messageApi, contextHolder] = message.useMessage();
1919

20+
//ref to track if user is finished typing
21+
const finishedTypingRef = useRef(false);
22+
const isFirstLoad = useRef(true);
23+
24+
//need to detect when user is finished typing to run check previous password validator, otherwise perofrmance is too slow
25+
useEffect(() => {
26+
const timer = setTimeout(() => {
27+
if (!isFirstLoad.current) {
28+
validatePassword(form.getFieldValue('newPassword'), true);
29+
finishedTypingRef.current = true;
30+
form.validateFields(['newPassword']);
31+
} else {
32+
isFirstLoad.current = false;
33+
}
34+
}, 1000);
35+
36+
return () => clearTimeout(timer);
37+
}, [form.getFieldValue('newPassword')]);
38+
39+
const validatePassword = (value, checkOldPassword) => {
40+
let pw = value;
41+
if (!value) {
42+
pw = '';
43+
}
44+
45+
if (userDetails) {
46+
if (checkOldPassword) {
47+
setPopOverContent(
48+
passwordComplexityValidator({
49+
password: pw,
50+
generateContent: true,
51+
user: userDetails,
52+
oldPasswordCheck: true,
53+
})
54+
);
55+
} else {
56+
setPopOverContent(passwordComplexityValidator({ password: pw, generateContent: true, user: userDetails }));
57+
}
58+
}
59+
};
60+
2061
const onLoad = async () => {
2162
//get user details from /api/auth//getUserDetailsWithToken/:token
2263
try {
@@ -116,9 +157,6 @@ const ResetPassword = () => {
116157
};
117158

118159
useEffect(() => {}, [popOverContent]);
119-
const validatePassword = (value) => {
120-
setPopOverContent(passwordComplexityValidator({ password: value, generateContent: true, user: userDetails }));
121-
};
122160

123161
return (
124162
<Form onFinish={onFinish} layout="vertical" form={form}>
@@ -137,12 +175,29 @@ const ResetPassword = () => {
137175
{ max: 64, message: 'Maximum of 64 characters allowed' },
138176
() => ({
139177
validator(_, value) {
178+
if (!value) {
179+
return Promise.reject();
180+
}
181+
//make sure it doesn't equal current password
182+
if (form.getFieldValue('currentPassword') === value) {
183+
return Promise.reject(new Error('New password cannot be the same as the current password!'));
184+
}
185+
let errors = [];
186+
187+
if (finishedTypingRef.current) {
188+
errors = passwordComplexityValidator({ password: value, user: userDetails, oldPasswordCheck: true });
189+
} else {
190+
errors = passwordComplexityValidator({ password: value, user: userDetails });
191+
}
192+
193+
finishedTypingRef.current = false;
194+
140195
//passwordComplexityValidator always returns an array with at least one attributes element
141-
const errors = passwordComplexityValidator({ password: value, user: userDetails });
142196
if (!value || errors.length === 1) {
143197
return Promise.resolve();
198+
} else {
199+
return Promise.reject(new Error('Password does not meet complexity requirements!'));
144200
}
145-
return Promise.reject(new Error('Password does not meet complexity requirements!'));
146201
},
147202
}),
148203
]}>
@@ -153,7 +208,10 @@ const ResetPassword = () => {
153208
validatePassword(e.target.value);
154209
}}
155210
onFocus={(e) => {
156-
validatePassword(e.target.value);
211+
validatePassword(e.target.value, true);
212+
}}
213+
onBlur={(e) => {
214+
validatePassword(e.target.value, true);
157215
}}
158216
/>
159217
</Form.Item>

Tombolo/client-reactjs/src/components/login/ResetTempPassword.js

+78-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React, { useState, useEffect, useRef } from 'react';
22
import { Form, Input, Button, Spin, message, Popover } from 'antd';
33
import { resetTempPassword } from './utils';
44
import passwordComplexityValidator from '../common/passwordComplexityValidator';
@@ -12,11 +12,52 @@ function ResetTempPassword() {
1212
const [userDetails, setUserDetails] = useState(null);
1313
const [form] = Form.useForm();
1414

15-
// For password validator pop over
16-
const validatePassword = (value) => {
17-
setPopOverContent(passwordComplexityValidator({ password: value, generateContent: true, user: userDetails }));
18-
};
15+
//ref to track if user is finished typing
16+
const finishedTypingRef = useRef(false);
17+
const isFirstLoad = useRef(true);
18+
19+
//need to detect when user is finished typing to run check previous password validator, otherwise perofrmance is too slow
20+
useEffect(() => {
21+
const timer = setTimeout(() => {
22+
if (!isFirstLoad.current) {
23+
validatePassword(form.getFieldValue('password'), true);
24+
finishedTypingRef.current = true;
25+
form.validateFields(['password']);
26+
} else {
27+
isFirstLoad.current = false;
28+
}
29+
}, 1000);
1930

31+
return () => clearTimeout(timer);
32+
}, [form.getFieldValue('password')]);
33+
34+
const validatePassword = (value, checkOldPassword) => {
35+
let pw = value;
36+
if (!value) {
37+
pw = '';
38+
}
39+
40+
if (checkOldPassword) {
41+
setPopOverContent(
42+
passwordComplexityValidator({
43+
password: pw,
44+
generateContent: true,
45+
user: userDetails,
46+
oldPasswordCheck: true,
47+
newUser: userDetails.newUser,
48+
})
49+
);
50+
} else {
51+
setPopOverContent(
52+
passwordComplexityValidator({
53+
password: pw,
54+
generateContent: true,
55+
user: userDetails,
56+
newUser: userDetails.newUser,
57+
})
58+
);
59+
}
60+
};
2061
// On component load, get the token from the URL
2162
useEffect(() => {
2263
const url = window.location.href;
@@ -120,12 +161,38 @@ function ResetTempPassword() {
120161
},
121162
() => ({
122163
validator(_, value) {
164+
if (!value) {
165+
return Promise.reject();
166+
}
167+
//make sure it doesn't equal current password
168+
if (form.getFieldValue('currentPassword') === value) {
169+
return Promise.reject(new Error('New password cannot be the same as the current password!'));
170+
}
171+
let errors = [];
172+
173+
if (finishedTypingRef.current) {
174+
errors = passwordComplexityValidator({
175+
password: value,
176+
user: userDetails,
177+
oldPasswordCheck: true,
178+
newUser: userDetails.newUser,
179+
});
180+
} else {
181+
errors = passwordComplexityValidator({
182+
password: value,
183+
user: userDetails,
184+
newUser: userDetails.newUser,
185+
});
186+
}
187+
188+
finishedTypingRef.current = false;
189+
123190
//passwordComplexityValidator always returns an array with at least one attributes element
124-
const errors = passwordComplexityValidator({ password: value, user: userDetails });
125191
if (!value || errors.length === 1) {
126192
return Promise.resolve();
193+
} else {
194+
return Promise.reject(new Error('Password does not meet complexity requirements!'));
127195
}
128-
return Promise.reject(new Error('Password does not meet complexity requirements!'));
129196
},
130197
}),
131198
]}>
@@ -136,7 +203,10 @@ function ResetTempPassword() {
136203
validatePassword(e.target.value);
137204
}}
138205
onFocus={(e) => {
139-
validatePassword(e.target.value);
206+
validatePassword(e.target.value, true);
207+
}}
208+
onBlur={(e) => {
209+
validatePassword(e.target.value, true);
140210
}}
141211
/>
142212
</Form.Item>

0 commit comments

Comments
 (0)