/*
* Copyright (C) 2023 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import { debounce } from "throttle-debounce";
import React, { useEffect, useMemo, useState } from "react";
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { FormGroup, FormHelperText, FormSection } from "@patternfly/react-core/dist/esm/components/Form/index.js";
import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
import { InputGroup, InputGroupItem } from "@patternfly/react-core/dist/esm/components/InputGroup/index.js";
import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
import { CheckCircleIcon } from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
import { ExclamationCircleIcon } from "@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon";
import { ExclamationTriangleIcon } from "@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon";
import { EyeIcon } from "@patternfly/react-icons/dist/esm/icons/eye-icon";
import { EyeSlashIcon } from "@patternfly/react-icons/dist/esm/icons/eye-slash-icon";
// eslint-disable-next-line camelcase
import { password_quality } from "cockpit-components-password.jsx";
const _ = cockpit.gettext;
export const ruleLength = {
check: (policy, password) => password.length >= policy["min-length"].v,
id: "length",
isError: true,
text: (policy) => cockpit.format(_("Must be at least $0 characters"), policy["min-length"].v),
};
/* Calculate the password quality levels based on the password policy
* If the policy specifies a 'is-strict' rule anything bellow the minimum specified by the policy
* is considered invalid
* @param {int} minQualility - the minimum quality level
* @return {array} - the password strengh levels
*/
const getStrengthLevels = (minQualility, isStrict) => {
const levels = [{
higher_bound: minQualility - 1,
icon: ,
id: "weak",
label: _("Weak"),
lower_bound: 0,
valid: !isStrict,
variant: "error",
}];
if (minQualility <= 69) {
levels.push({
higher_bound: 69,
icon: ,
id: "medium",
label: _("Medium"),
lower_bound: minQualility,
valid: true,
variant: "warning",
});
}
levels.push({
higher_bound: 100,
icon: ,
id: "strong",
label: _("Strong"),
lower_bound: Math.max(70, minQualility),
valid: true,
variant: "success",
});
return levels;
};
const getRuleResults = (rules, policy, password) => {
return rules.map(rule => {
return {
id: rule.id,
isError: rule.isError,
isSatisfied: password.length > 0 ? rule.check(policy, password) : null,
text: rule.text(policy, password)
};
});
};
const rulesSatisfied = ruleResults => ruleResults.every(r => r.isSatisfied || !r.isError);
const passwordStrengthLabel = (idPrefix, strength, strengthLevels) => {
const level = strengthLevels.filter(l => l.id === strength)[0];
if (level) {
return (
{level.label}
);
}
};
export const PasswordFormFields = ({
confirmPassword,
confirmPasswordLabel,
emptyPasswordHelperText,
idPrefix,
password,
passwordLabel,
passwordRef,
policy,
rules,
sectionDescription,
setConfirmPassword,
setIsValid,
setPassword,
}) => {
const [passwordHidden, setPasswordHidden] = useState(true);
const [confirmHidden, setConfirmHidden] = useState(true);
const [checkPassword, setCheckPassword] = useState(password);
const [checkConfirmPassword, setCheckConfirmPassword] = useState(confirmPassword);
const [passwordStrength, setPasswordStrength] = useState("");
useEffect(() => {
debounce(300, () => setCheckPassword(password))();
}, [password, setCheckPassword]);
useEffect(() => {
debounce(300, () => setCheckConfirmPassword(confirmPassword))();
}, [confirmPassword, setCheckConfirmPassword]);
const ruleResults = useMemo(() => {
return getRuleResults(rules, policy, checkPassword);
}, [policy, checkPassword, rules]);
const allowEmptyPassword = policy?.["allow-empty"]?.v ?? false;
const bothFieldsEmpty = (checkPassword?.length ?? 0) === 0 && (checkConfirmPassword?.length ?? 0) === 0;
const isEmptyPasswordAccepted = allowEmptyPassword && bothFieldsEmpty;
const ruleConfirmMatches = bothFieldsEmpty
? (allowEmptyPassword ? true : null)
: checkPassword === checkConfirmPassword;
const ruleHelperItems = ruleResults.map(rule => {
let variant = rule.isSatisfied === null ? "indeterminate" : rule.isSatisfied ? "success" : "error";
if (!rule.isError) {
if (rule.isSatisfied || rule.isSatisfied === null) {
return null;
}
variant = "warning";
}
return (
{rule.text}
);
});
const ruleConfirmVariant = ruleConfirmMatches === null ? "indeterminate" : ruleConfirmMatches ? "success" : "error";
const strengthLevels = useMemo(() => {
return policy && getStrengthLevels(policy["min-quality"].v, policy["is-strict"].v);
}, [policy]);
useEffect(() => {
const updatePasswordStrength = async () => {
const _passwordStrength = await getPasswordStrength(checkPassword, strengthLevels);
setPasswordStrength(_passwordStrength);
};
updatePasswordStrength();
}, [checkPassword, strengthLevels]);
useEffect(() => {
const isStrict = policy?.["is-strict"]?.v ?? false;
if (isEmptyPasswordAccepted) {
setIsValid(true);
return;
}
if (!ruleConfirmMatches) {
setIsValid(false);
return;
}
if (isStrict) {
setIsValid(!!(rulesSatisfied(ruleResults) && isValidStrength(passwordStrength, strengthLevels)));
return;
}
// Non-strict: length/strength are shown but not required; only confirmation must match.
setIsValid((checkPassword?.length ?? 0) > 0);
}, [
checkPassword,
isEmptyPasswordAccepted,
policy,
ruleConfirmMatches,
ruleResults,
passwordStrength,
setIsValid,
strengthLevels,
]);
return (
{sectionDescription}
setPassword(val)}
id={idPrefix + "-password-field"}
ref={passwordRef}
/>
{ruleHelperItems}
{allowEmptyPassword && emptyPasswordHelperText && (
{emptyPasswordHelperText}
)}
setConfirmPassword(val)}
id={idPrefix + "-password-confirm-field"}
/>
{_("Passphrases must match")}
);
};
const getPasswordStrength = async (password, strengthLevels) => {
// In case of unacceptable password just return 0
const force = true;
const quality = await password_quality(password, force);
const level = strengthLevels.filter(l => l.lower_bound <= quality.value && l.higher_bound >= quality.value)[0];
return level.id;
};
const isValidStrength = (strength, strengthLevels) => {
const level = strengthLevels.filter(l => l.id === strength)[0];
return level ? level.valid : false;
};