LCOV - code coverage report
Current view: top level - pkg/users - expiration-dialogs.js Coverage Total Hit
Test: cockpit Lines: 1.7 % 175 3
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              : 
       6           19 : import cockpit from 'cockpit';
       7           19 : import React from 'react';
       8              : import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
       9              : import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
      10              : import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
      11              : import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
      12              : import { DatePicker } from "@patternfly/react-core/dist/esm/components/DatePicker/index.js";
      13              : 
      14              : import { has_errors } from "./dialog-utils.js";
      15              : import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx";
      16              : import * as timeformat from "timeformat";
      17              : import { FormHelper } from "cockpit-components-form-helper";
      18              : 
      19           19 : const _ = cockpit.gettext;
      20              : 
      21            0 : function AccountExpirationDialogBody({ state, errors, change, validate, update }) {
      22            0 :     const { mode, before, date } = state;
      23              : 
      24            0 :     return (
      25            0 :         <Form className="expiration-modal" onSubmit={apply_modal_dialog}>
      26            0 :             <FormGroup validated={errors?.date ? "error" : "default"}>
      27            0 :                 <Radio id="account-expiration-never" name="mode" value="never"
      28            0 :                        label={_("Never expire account")}
      29            0 :                        isChecked={mode == "never"} onChange={() => change("mode", "never")} />
      30            0 :                 <Radio id="account-expiration-expires" name="mode" value="expires"
      31            0 :                        label={
      32            0 :                            <Flex>
      33            0 :                                <span>{before}</span>
      34            0 :                                <DatePicker aria-label={_("Pick date")}
      35            0 :                                            buttonAriaLabel={_("Toggle date picker")}
      36            0 :                                            locale={timeformat.dateFormatLang()}
      37            0 :                                            weekStart={timeformat.firstDayOfWeek()}
      38            0 :                                            onChange={(_, str) => change("date", str)}
      39            0 :                                            onBlur={() => { validate(); update() }}
      40            0 :                                            invalidFormatText=""
      41            0 :                                            id="account-expiration-input"
      42            0 :                                            value={date}
      43            0 :                                            appendTo={() => document.body}
      44            0 :                                            isDisabled={mode !== "expires"} />
      45            0 :                            </Flex>
      46              :                        }
      47            0 :                        isChecked={mode == "expires"} onChange={() => change("mode", "expires")} />
      48            0 :                 <FormHelper helperTextInvalid={errors?.date} />
      49            0 :             </FormGroup>
      50            0 :         </Form>
      51              :     );
      52            0 : }
      53              : 
      54            0 : export function account_expiration_dialog(account, expire_date) {
      55            0 :     let dlg = null;
      56              : 
      57            0 :     const parts = _("Expire account on");
      58              : 
      59            0 :     const state = {
      60            0 :         mode: expire_date ? "expires" : "never",
      61            0 :         before: parts,
      62            0 :         date: expire_date?.toISOString().substring(0, 10) ?? ""
      63            0 :     };
      64              : 
      65            0 :     let errors = { };
      66              : 
      67            0 :     function change(field, value) {
      68            0 :         state[field] = value;
      69            0 :         errors = { };
      70            0 :         update();
      71            0 :     }
      72              : 
      73              :     // Datepicker does not provide information about the validity of the date so we need to do it here
      74              :     // https://github.com/patternfly/patternfly-react/issues/5564
      75            0 :     function validate() {
      76            0 :         errors = { };
      77              : 
      78            0 :         if (state.mode == "expires") {
      79            0 :             if (!state.date)
      80            0 :                 errors.date = _("Please specify an expiration date");
      81            0 :             else {
      82            0 :                 const date = new Date(state.date + "T12:00:00Z");
      83            0 :                 if (isNaN(date.getTime()) || date.getTime() < 0)
      84            0 :                     errors.date = _("Invalid expiration date");
      85            0 :             }
      86            0 :         }
      87              : 
      88            0 :         return !has_errors(errors);
      89            0 :     }
      90              : 
      91            0 :     function update() {
      92            0 :         const props = {
      93            0 :             id: "account-expiration",
      94            0 :             title: _("Account expiration"),
      95            0 :             body: <AccountExpirationDialogBody state={state} errors={errors} change={change} validate={validate} update={update} />
      96            0 :         };
      97              : 
      98            0 :         const footer = {
      99            0 :             actions: [
     100            0 :                 {
     101            0 :                     caption: _("Change"),
     102            0 :                     style: "primary",
     103            0 :                     clicked: () => {
     104            0 :                         if (validate()) {
     105            0 :                             const prog = ["/usr/sbin/usermod", "-e"];
     106            0 :                             if (state.mode == "expires") {
     107            0 :                                 const date = new Date(state.date + "T12:00:00Z");
     108            0 :                                 prog.push(date.toISOString().substring(0, 10));
     109            0 :                             } else
     110            0 :                                 prog.push("");
     111            0 :                             prog.push(account.name);
     112            0 :                             return cockpit.spawn(prog, { superuser: "require", err: "message" });
     113            0 :                         } else {
     114            0 :                             update();
     115            0 :                             return Promise.reject();
     116            0 :                         }
     117            0 :                     }
     118            0 :                 }
     119            0 :             ]
     120            0 :         };
     121              : 
     122            0 :         if (!dlg)
     123            0 :             dlg = show_modal_dialog(props, footer);
     124            0 :         else {
     125            0 :             dlg.setProps(props);
     126            0 :             dlg.setFooterProps(footer);
     127            0 :         }
     128            0 :     }
     129              : 
     130            0 :     update();
     131            0 : }
     132              : 
     133            0 : function PasswordExpirationDialogBody({ state, errors, change }) {
     134            0 :     const { mode, before, after, days } = state;
     135              : 
     136            0 :     return (
     137            0 :         <Form className="expiration-modal" onSubmit={apply_modal_dialog}>
     138            0 :             <FormGroup>
     139            0 :                 <Radio id="password-expiration-never" name="mode" value="never"
     140            0 :                        label={_("Never expire password")}
     141            0 :                        isChecked={mode == "never"} onChange={() => change("mode", "never")} />
     142            0 :                 <Radio id="password-expiration-expires" name="mode" value="expires"
     143            0 :                        label={<>
     144            0 :                            <span id="password-expiration-before">{before}</span>
     145            0 :                            <TextInput className="size-text-ct" id="password-expiration-input"
     146            0 :                                   validated={(errors?.days) ? "error" : "default"}
     147            0 :                                   value={days} onChange={(_event, value) => change("days", value)} isDisabled={mode != "expires"} />
     148            0 :                            <span id="password-expiration-after">{after}</span>
     149            0 :                        </>}
     150            0 :                        isChecked={mode == "expires"} onChange={() => change("mode", "expires")} />
     151            0 :                 <FormHelper helperTextInvalid={errors?.days} />
     152            0 :             </FormGroup>
     153            0 :         </Form>
     154              :     );
     155            0 : }
     156              : 
     157            0 : export function password_expiration_dialog(account, expire_days) {
     158            0 :     let dlg = null;
     159              : 
     160              :     /* TRANSLATORS: This is split up and therefore cannot use ngettext plurals */
     161            0 :     const parts = _("Require password change every $0 days").split("$0");
     162              : 
     163            0 :     const state = {
     164            0 :         mode: expire_days ? "expires" : "never",
     165            0 :         before: parts[0],
     166            0 :         after: parts[1],
     167            0 :         days: expire_days || ""
     168            0 :     };
     169              : 
     170            0 :     let errors = { };
     171              : 
     172            0 :     function change(field, value) {
     173            0 :         state[field] = value;
     174            0 :         update();
     175            0 :     }
     176              : 
     177            0 :     function validate() {
     178            0 :         errors = { };
     179              : 
     180            0 :         if (state.mode == "expires") {
     181            0 :             const days = parseInt(state.days);
     182            0 :             if (isNaN(days) || days < 0)
     183            0 :                 errors.days = _("Invalid number of days");
     184            0 :         }
     185              : 
     186            0 :         return !has_errors(errors);
     187            0 :     }
     188              : 
     189            0 :     function update() {
     190            0 :         const props = {
     191            0 :             id: "password-expiration",
     192            0 :             title: _("Password expiration"),
     193            0 :             body: <PasswordExpirationDialogBody state={state} errors={errors} change={change} />,
     194            0 :             variant: "small"
     195            0 :         };
     196              : 
     197            0 :         const footer = {
     198            0 :             actions: [
     199            0 :                 {
     200            0 :                     caption: _("Change"),
     201            0 :                     style: "primary",
     202            0 :                     clicked: () => {
     203            0 :                         if (validate()) {
     204            0 :                             const days = state.mode == "expires" ? parseInt(state.days) : -1;
     205            0 :                             return cockpit.spawn(["passwd", "-x", String(days), account.name],
     206            0 :                                                  { superuser: "require", err: "message" });
     207            0 :                         } else {
     208            0 :                             update();
     209            0 :                             return Promise.reject();
     210            0 :                         }
     211            0 :                     }
     212            0 :                 }
     213            0 :             ]
     214            0 :         };
     215              : 
     216            0 :         if (!dlg)
     217            0 :             dlg = show_modal_dialog(props, footer);
     218            0 :         else {
     219            0 :             dlg.setProps(props);
     220            0 :             dlg.setFooterProps(footer);
     221            0 :         }
     222            0 :     }
     223              : 
     224            0 :     update();
     225            0 : }
        

Generated by: LCOV version 2.0-1