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