LCOV - code coverage report
Current view: top level - pkg/lib - cockpit-components-password.tsx Coverage Total Hit
Test: cockpit Lines: 74.1 % 139 103
Test Date: 2026-07-13 10:00:01

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2020 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5           19 : import cockpit from 'cockpit';
       6           19 : 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           19 : 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 : };
        

Generated by: LCOV version 2.0-1