Line data Source code
1 : /*
2 : * Copyright (C) 2020 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 18 : import cockpit from 'cockpit';
6 18 : import React, { useState } from 'react';
7 : import { debounce } from 'throttle-debounce';
8 : import { Button } from '@patternfly/react-core/dist/esm/components/Button/index.js';
9 : import { FormGroup, FormHelperText } from "@patternfly/react-core/dist/esm/components/Form/index.js";
10 : import { InputGroup, InputGroupItem } from '@patternfly/react-core/dist/esm/components/InputGroup/index.js';
11 : import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
12 : import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
13 : import { Progress, ProgressMeasureLocation, ProgressSize, type ProgressProps } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
14 : import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
15 : import { EyeIcon, EyeSlashIcon, HelpIcon } from '@patternfly/react-icons';
16 : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
17 :
18 : import { FormHelper } from "cockpit-components-form-helper";
19 :
20 : import './cockpit-components-password.scss';
21 :
22 18 : const _ = cockpit.gettext;
23 :
24 : interface PasswordQuality {
25 : value: number;
26 : message?: string | undefined;
27 : }
28 :
29 5 : export function password_quality(password: string, force?: boolean): Promise<PasswordQuality> {
30 5 : return new Promise((resolve, reject) => {
31 5 : cockpit.spawn(['/usr/bin/pwscore'], { err: "message" })
32 5 : .input(password)
33 5 : .done(function(content) {
34 5 : const quality = parseInt(content, 10);
35 0 : if (quality === 0 && !force)
36 0 : reject(new Error(_("Password is too weak")));
37 : else
38 0 : resolve({ value: quality, message: quality === 100 ? _("Excellent password") : undefined });
39 5 : })
40 0 : .fail(function(ex) {
41 0 : if (!force)
42 0 : reject(new Error(ex.message || _("Password is not acceptable")));
43 : else
44 0 : resolve({ value: 0 });
45 0 : });
46 5 : });
47 5 : }
48 :
49 5 : const debounced_password_quality = debounce(300, (value: string, callback: (quality: PasswordQuality) => void) => {
50 0 : password_quality(value).catch(() => ({ value: 0 })).then(callback);
51 5 : });
52 :
53 5 : export const PasswordFormFields = ({
54 5 : password_label, password_confirm_label,
55 5 : password_label_info,
56 5 : initial_password,
57 5 : error_password, error_password_confirm,
58 5 : idPrefix, change
59 5 : }: {
60 : password_label: string,
61 : password_confirm_label?: string,
62 : password_label_info?: string,
63 : initial_password?: string,
64 : error_password?: string | undefined,
65 : error_password_confirm?: string | undefined,
66 : idPrefix: string,
67 : change: (field: "password" | "password_confirm", value: string) => void,
68 5 : }) => {
69 5 : const [password, setPassword] = useState(initial_password || "");
70 5 : const [passwordConfirm, setConfirmPassword] = useState("");
71 5 : const [passwordStrength, setPasswordStrength] = useState<number>(-1);
72 5 : const [passwordMessage, setPasswordMessage] = useState<string | undefined>("");
73 5 : const [passwordHidden, setPasswordHidden] = useState(true);
74 5 : const [passwordConfirmHidden, setPasswordConfirmHidden] = useState(true);
75 :
76 5 : function onPasswordChanged(value: string) {
77 5 : setPassword(value);
78 5 : change("password", value);
79 :
80 5 : if (value) {
81 5 : debounced_password_quality(value, (strength: PasswordQuality) => {
82 5 : setPasswordStrength(strength.value);
83 5 : setPasswordMessage(strength.message);
84 5 : });
85 0 : } else {
86 0 : setPasswordStrength(-1);
87 0 : setPasswordMessage("");
88 0 : }
89 5 : }
90 :
91 5 : let variant: ProgressProps['variant'];
92 5 : let message;
93 5 : let messageColor;
94 4 : if (passwordStrength > 66) {
95 4 : variant = "success";
96 4 : messageColor = "pf-v6-u-success-color-200";
97 4 : message = _("Strong password");
98 0 : } else if (passwordStrength > 33) {
99 1 : variant = "warning";
100 1 : messageColor = "pf-v6-u-warning-color-200";
101 1 : message = _("Acceptable password");
102 1 : } else {
103 5 : variant = "danger";
104 5 : messageColor = "pf-v6-u-danger-color-200";
105 5 : message = _("Weak password");
106 5 : }
107 :
108 5 : if (!passwordMessage && message)
109 5 : setPasswordMessage(message);
110 :
111 5 : let passwordStrengthValue = passwordStrength;
112 5 : if (password !== "" && (passwordStrengthValue >= 0 && passwordStrengthValue < 25))
113 0 : passwordStrengthValue = 25;
114 :
115 5 : return (
116 5 : <>
117 5 : <FormGroup label={password_label}
118 0 : {...password_label_info && {
119 0 : labelHelp: <Popover bodyContent={password_label_info}>
120 0 : <Button icon={<HelpIcon />} id={`${password_label}-help-popup-button`} variant="plain" aria-label="Help" />
121 0 : </Popover>
122 0 : }}
123 5 : id={idPrefix + "-pw1-group"}
124 5 : fieldId={idPrefix + "-pw1"}>
125 5 : <InputGroup>
126 5 : <InputGroupItem isFill>
127 0 : <TextInput className="check-passwords" type={passwordHidden ? "password" : "text"} id={idPrefix + "-pw1"}
128 5 : autoComplete="new-password" value={password} onChange={(_event, value) => onPasswordChanged(value)}
129 0 : validated={error_password ? "warning" : "default"} />
130 5 : </InputGroupItem>
131 5 : <InputGroupItem>
132 5 : <Button
133 5 : variant="control"
134 0 : onClick={() => setPasswordHidden(!passwordHidden)}
135 0 : aria-label={passwordHidden ? _("Show password") : _("Hide password")}>
136 0 : {passwordHidden ? <EyeIcon /> : <EyeSlashIcon />}
137 5 : </Button>
138 5 : </InputGroupItem>
139 5 : </InputGroup>
140 5 : {passwordStrengthValue >= 0 && <Flex spaceItems={{ default: 'spaceItemsSm' }}>
141 5 : <FlexItem>
142 5 : <Progress id={idPrefix + "-meter"}
143 5 : className={"pf-v6-u-pt-xs ct-password-strength-meter " + variant}
144 5 : title={_("password quality")}
145 5 : size={ProgressSize.sm}
146 5 : measureLocation={ProgressMeasureLocation.none}
147 5 : variant={variant}
148 5 : value={passwordStrengthValue} />
149 5 : </FlexItem>
150 5 : <FlexItem>
151 5 : <div id={idPrefix + "-password-meter-message"} className={"pf-v6-c-form__helper-text " + messageColor} aria-live="polite">{passwordMessage}</div>
152 5 : </FlexItem>
153 5 : </Flex>}
154 0 : {error_password && <FormHelperText>
155 0 : <HelperText component="ul" aria-live="polite" id="password-error-message">
156 0 : <HelperTextItem variant="warning" component="li">
157 0 : {error_password}
158 0 : </HelperTextItem>
159 0 : </HelperText>
160 0 : </FormHelperText>}
161 5 : </FormGroup>
162 :
163 5 : {password_confirm_label && <FormGroup label={password_confirm_label}
164 5 : id={idPrefix + "-pw2-group"}
165 5 : fieldId={idPrefix + "-pw2"}>
166 5 : <InputGroup>
167 5 : <InputGroupItem isFill>
168 0 : <TextInput type={passwordConfirmHidden ? "password" : "text"} id={idPrefix + "-pw2"} autoComplete="new-password"
169 5 : value={passwordConfirm} onChange={(_event, value) => { setConfirmPassword(value); change("password_confirm", value) }} />
170 5 : </InputGroupItem>
171 5 : <InputGroupItem>
172 5 : <Button
173 5 : variant="control"
174 0 : onClick={() => setPasswordConfirmHidden(!passwordConfirmHidden)}
175 0 : aria-label={passwordConfirmHidden ? _("Show confirmation password") : _("Hide confirmation password")}>
176 0 : {passwordConfirmHidden ? <EyeIcon /> : <EyeSlashIcon />}
177 5 : </Button>
178 5 : </InputGroupItem>
179 5 : </InputGroup>
180 5 : <FormHelper fieldId={idPrefix + "-pw2"} helperTextInvalid={error_password_confirm} />
181 5 : </FormGroup>}
182 5 : </>
183 : );
184 5 : };
|