diff --git a/src/actions.js b/src/actions.js
index 6d3e325..a7f0928 100644
--- a/src/actions.js
+++ b/src/actions.js
@@ -330,6 +330,13 @@ export function fetchUsernameLength() {
return graphql(payload, `USERNAME_LENGTH_FIELDS`);
}
+export function fetchPasswordPolicy() {
+ const payload = `query {
+ passwordPolicy
+ }`;
+ return graphql(payload, "PASSWORD_POLICY_FIELDS");
+}
+
export function usernameValidationClear() {
return (dispatch) => {
dispatch({ type: `USERNAME_FIELDS_VALIDATION_CLEAR` });
diff --git a/src/components/UserForm.js b/src/components/UserForm.js
index a313c64..53d2146 100644
--- a/src/components/UserForm.js
+++ b/src/components/UserForm.js
@@ -30,6 +30,7 @@ import {
fetchObligatoryUserFields,
fetchObligatoryEnrolmentOfficerFields,
fetchUsernameLength,
+ fetchPasswordPolicy,
} from "../actions";
import UserMasterPanel from "./UserMasterPanel";
@@ -69,6 +70,9 @@ class UserForm extends Component {
if (!this.state.usernameLength) {
this.props.fetchUsernameLength();
}
+ if (!this.state.passwordPolicy) {
+ this.props.fetchPasswordPolicy();
+ }
}
componentWillUnmount() {
@@ -212,6 +216,7 @@ class UserForm extends Component {
obligatoryUserFields,
obligatoryEoFields,
usernameLength,
+ passwordPolicy
} = this.props;
const { user, isSaved, reset } = this.state;
@@ -255,6 +260,7 @@ class UserForm extends Component {
obligatory_user_fields={obligatoryUserFields}
obligatory_eo_fields={obligatoryEoFields}
usernameLength={usernameLength}
+ passwordPolicy={passwordPolicy}
/>
)}
@@ -277,6 +283,7 @@ const mapStateToProps = (state) => ({
isUserNameValid: state.admin.validationFields?.username?.isValid,
isUserEmailValid: state.admin.validationFields?.userEmail?.isValid,
usernameLength: state.admin?.usernameLength,
+ passwordPolicy: state.admin?.passwordPolicy,
isUserEmailFormatInvalid: state.admin.validationFields?.userEmailFormat?.isInvalid,
});
@@ -291,6 +298,7 @@ const mapDispatchToProps = (dispatch) =>
fetchObligatoryUserFields,
fetchObligatoryEnrolmentOfficerFields,
fetchUsernameLength,
+ fetchPasswordPolicy,
journalize,
coreConfirm,
},
diff --git a/src/components/UserMasterPanel.js b/src/components/UserMasterPanel.js
index 2d2fef8..eda630a 100644
--- a/src/components/UserMasterPanel.js
+++ b/src/components/UserMasterPanel.js
@@ -23,9 +23,11 @@ import {
userEmailValidationClear,
setUserEmailValid,
saveEmailFormatValidity,
+ fetchPasswordPolicy,
} from "../actions";
import { passwordGenerator } from "../helpers/passwordGenerator";
+import { validatePassword } from "../helpers/passwordValidator";
const styles = (theme) => ({
tableTitle: theme.table.title,
@@ -59,9 +61,15 @@ const UserMasterPanel = (props) => {
savedUsername,
savedUserEmail,
usernameLength,
+ passwordPolicy,
} = props;
- const { formatMessage } = useTranslations("admin", modulesManager);
+ const { formatMessage, formatMessageWithValues } = useTranslations("admin", modulesManager);
const dispatch = useDispatch();
+
+ useEffect(() => {
+ dispatch(fetchPasswordPolicy());
+ }, [dispatch]);
+
const renderLastNameFirst = modulesManager.getConf(
"fe-insuree",
"renderLastNameFirst",
@@ -97,13 +105,23 @@ const UserMasterPanel = (props) => {
handleEmailChange(edited?.email);
}, []);
+ const [passwordFeedback, setPasswordFeedback] = useState("");
+ const [passwordScore, setPasswordScore] = useState(0);
const [showPassword, setShowPassword] = useState(false);
+ const IS_PASSWORD_SECURED = passwordScore >= 2;
const handleClickShowPassword = () => setShowPassword((show) => !show);
const handleMouseDownPassword = (event) => {
event.preventDefault();
};
+ const handlePasswordChange = (password) => {
+ const { feedback, score } = validatePassword(password, passwordPolicy, formatMessage, formatMessageWithValues);
+ setPasswordFeedback(feedback);
+ setPasswordScore(score);
+ onEditedChanged({ ...edited, password });
+ };
+
const generatePassword = () => {
const passwordGeneratorOptions = modulesManager.getConf("fe-admin", "passwordGeneratorOptions", {
length: 10,
@@ -181,47 +199,47 @@ const UserMasterPanel = (props) => {
obligatoryUserFields?.email == "H" ||
(edited.userTypes?.includes(ENROLMENT_OFFICER_USER_TYPE) && obligatoryEOFields?.email == "H")
) && (
-
- handleEmailChange(email)}
- />
-
- )}
+
+ handleEmailChange(email)}
+ />
+
+ )}
{!(
obligatoryUserFields?.phone == "H" ||
(edited.userTypes?.includes(ENROLMENT_OFFICER_USER_TYPE) && obligatoryEOFields?.phone == "H")
) && (
-
- onEditedChanged({ ...edited, phoneNumber })}
- />
-
- )}
+
+ onEditedChanged({ ...edited, phoneNumber })}
+ />
+
+ )}
{
label="user.newPassword"
readOnly={readOnly}
value={edited.password}
- onChange={(password) => onEditedChanged({ ...edited, password })}
+ onChange={(password) => {
+ handlePasswordChange(password);
+ }}
endAdornment={
{
}
/>
+
+ {passwordFeedback}
+
{
+ if (condition) {
+ suggestions.push(message);
+ }
+};
+
+export const generateFeedback = (suggestions, formatMessageWithValues) => {
+ if (suggestions.length > 0) {
+ const requirements = suggestions.join(', ');
+ const formattedMessage = formatMessageWithValues("admin.password.requirements", { requirements });
+ return { feedback: formattedMessage, score: 0 };
+ }
+ return null;
+};
+
+export const validatePassword = (password, passwordPolicy, formatMessage, formatMessageWithValues) => {
+ if (!passwordPolicy || !password) {
+ return { feedback: "", score: 0 };
+ }
+
+ const jsonPasswordPolicy = JSON.parse(passwordPolicy);
+ const suggestions = [];
+
+ const {
+ min_length,
+ require_lower_case,
+ require_upper_case,
+ require_numbers,
+ require_special_characters
+ } = jsonPasswordPolicy || {};
+
+ addSuggestion(suggestions, password.length < min_length, formatMessageWithValues("admin.password.minLength", { count: min_length }));
+ addSuggestion(suggestions, require_lower_case && !/[a-z]/.test(password), formatMessageWithValues("admin.password.lowerCase", { count: require_lower_case }));
+ addSuggestion(suggestions, require_upper_case && !/[A-Z]/.test(password), formatMessageWithValues("admin.password.upperCase", { count: require_upper_case }));
+ addSuggestion(suggestions, require_numbers && !/[0-9]/.test(password), formatMessageWithValues("admin.password.numbers", { count: require_numbers }));
+ addSuggestion(suggestions, require_special_characters && !/[^a-zA-Z0-9]/.test(password), formatMessageWithValues("admin.password.specialCharacters", { count: require_special_characters }));
+
+ const feedbackResult = generateFeedback(suggestions, formatMessageWithValues);
+ if (feedbackResult) {
+ return feedbackResult;
+ }
+
+ const result = zxcvbn(password);
+ const feedback = result.feedback.suggestions.join(" ") || formatMessage("admin.password.strong");
+ const score = result.score;
+
+ let scoreDescription;
+ switch (score) {
+ case 1:
+ scoreDescription = `${formatMessage("admin.password.weak")}. ${feedback}`;
+ break;
+ case 2:
+ scoreDescription = `${formatMessage("admin.password.medium")}. ${feedback}`;
+ break;
+ case 3:
+ scoreDescription = `${formatMessage("admin.password.strong")}`;
+ break;
+ case 4:
+ scoreDescription = `${formatMessage("admin.password.veryStrong")}`;
+ break;
+ default:
+ scoreDescription = formatMessage("admin.password.unknownScore");
+ }
+
+ return {
+ feedback: scoreDescription,
+ score,
+ };
+};
diff --git a/src/reducer.js b/src/reducer.js
index 26dc93f..47e5c39 100644
--- a/src/reducer.js
+++ b/src/reducer.js
@@ -477,6 +477,28 @@ function reducer(
fetchingUsernameLength: false,
errorUsernameLength: formatServerError(action.payload),
};
+ case "PASSWORD_POLICY_FIELDS_REQ":
+ return {
+ ...state,
+ fetchingPasswordPolicy: true,
+ fetchedPasswordPolicy: false,
+ passwordPolicy: null,
+ errorPasswordPolicy: null,
+ };
+ case "PASSWORD_POLICY_FIELDS_RESP":
+ return {
+ ...state,
+ fetchingPasswordPolicy: false,
+ fetchedPasswordPolicy: true,
+ passwordPolicy: action.payload.data.passwordPolicy,
+ errorPasswordPolicy: formatGraphQLError(action.payload),
+ };
+ case "PASSWORD_POLICY_FIELDS_ERR":
+ return {
+ ...state,
+ fetchingPasswordPolicy: false,
+ errorPasswordPolicy: formatServerError(action.payload),
+ };
case "ADMIN_USER_MUTATION_REQ":
return dispatchMutationReq(state, action);
case "ADMIN_USER_MUTATION_ERR":
diff --git a/src/translations/en.json b/src/translations/en.json
index 4aaf670..705be09 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -82,5 +82,16 @@
"admin.EnrolmentOfficerPicker.openText": "Open",
"admin.EnrolmentOfficerPicker.closeText": "Close",
"admin.UserFilter.showHistory": "Show History",
- "admin.UserFilter.showDeleted": "Show Deleted"
+ "admin.UserFilter.showDeleted": "Show Deleted",
+ "admin.password.minLength": "at least {count} characters",
+ "admin.password.lowerCase": "{count, plural, one {# lowercase letter} other {# lowercase letters}}",
+ "admin.password.upperCase": "{count, plural, one {# uppercase letter} other {# uppercase letters}}",
+ "admin.password.numbers": "{count, plural, one {# number} other {# numbers}}",
+ "admin.password.specialCharacters": "{count, plural, one {# special character} other {# special characters}}",
+ "admin.password.requirements": "Password must include: {requirements}.",
+ "admin.password.weak": "Weak",
+ "admin.password.medium": "Medium",
+ "admin.password.strong": "Password is strong.",
+ "admin.password.veryStrong": "Password is very strong.",
+ "admin.password.unknownScore": "Unknown score."
}