/*
* 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, { useContext, useEffect, useMemo, useRef, useState } from "react";
import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
import { Form, 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 } from "@patternfly/react-core/dist/esm/components/InputGroup/index.js";
import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js";
import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
import { useWizardFooter } from "@patternfly/react-core/dist/esm/components/Wizard/index.js";
import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import {
guessUsernameFromFullName,
} from "../../apis/users.js";
import {
applyUsersPatch,
} from "../../actions/users-actions.js";
import {
applyAccounts,
} from "../../helpers/users.js";
import { PageContext, RuntimeContext, UsersContext } from "../../contexts/Common.jsx";
import { AnacondaWizardFooter } from "../AnacondaWizardFooter.jsx";
import { PasswordFormFields, ruleLength } from "../Password.jsx";
import "./Accounts.scss";
const _ = cockpit.gettext;
/** Read-only summary when user(s) are specified by kickstart (like GTK User Creation greyed out). */
const UsersReadOnlySummary = ({ users }) => {
if (!users?.length) return null;
return (
{cockpit.ngettext(
"The following user will be created:",
"The following users will be created:",
users.length
)}
{users.map((u, i) => (
{u.gecos ? `${u.gecos} (${u.name || ""})` : (u.name || "—")}
))}
);
};
/** Review step summary for account / root choices (see users `Page.review`). */
export const AccountsReviewDescription = () => {
const accounts = useContext(UsersContext);
const hasUsers = (accounts.users?.length ?? 0) > 0;
const userSummary = hasUsers
? (
)
: null;
if (accounts.isRootEnabled && !hasUsers) {
return _("Root account is enabled, but no user account has been configured");
}
if (!accounts.isRootEnabled && hasUsers) {
return userSummary;
}
if (accounts.isRootEnabled && hasUsers) {
return (
{_("Root account is enabled")}
{userSummary}
);
}
return null;
};
const reservedNames = [
"root",
"bin",
"daemon",
"adm",
"lp",
"sync",
"shutdown",
"halt",
"mail",
"operator",
"games",
"ftp",
"nobody",
"home",
"system",
];
const isUserNameWithInvalidCharacters = (userName) => {
return (
userName === "." ||
userName === ".." ||
userName.match(/^[0-9]+$/) ||
!userName.match(/^[A-Za-z0-9._][A-Za-z0-9._-]{0,30}([A-Za-z0-9._-]|\$)?$/)
);
};
const CreateAccount = ({
idPrefix,
setAccounts,
setIsUserValid,
setSkipAccountCreation,
skipAccountCreation,
}) => {
const accounts = useContext(UsersContext);
const firstUser = accounts.users?.[0] ?? {};
const [fullName, setFullName] = useState(firstUser.gecos ?? "");
const [checkFullName, setCheckFullName] = useState(firstUser.gecos ?? "");
const [fullNameInvalidHint, setFullNameInvalidHint] = useState("");
const [isFullNameValid, setIsFullNameValid] = useState(null);
const [userName, setUserName] = useState(firstUser.name ?? "");
const [checkUserName, setCheckUserName] = useState(firstUser.name ?? "");
const [userNameInvalidHint, setUserNameInvalidHint] = useState("");
const [isUserNameValid, setIsUserNameValid] = useState(null);
const [password, setPassword] = useState(accounts.password);
const [confirmPassword, setConfirmPassword] = useState(accounts.confirmPassword);
const [isPasswordValid, setIsPasswordValid] = useState(false);
const passwordPolicy = useContext(RuntimeContext).passwordPolicies.user;
const [guessingUserName, setGuessingUserName] = useState(false);
useEffect(() => {
debounce(300, () => setCheckUserName(userName))();
}, [userName, setCheckUserName]);
useEffect(() => {
debounce(300, () => setCheckFullName(fullName))();
}, [fullName, setCheckFullName]);
// null means the field is empty: keep the default (non-error) input style on load.
// User name and password must be set (or explicitly accepted empty), so require
// isUserNameValid === true and isPasswordValid === true. Full name may be left
// empty, so only reject explicit errors (isFullNameValid !== false).
useEffect(() => {
setIsUserValid(
(isPasswordValid === true && isUserNameValid === true && isFullNameValid !== false) ||
skipAccountCreation
);
}, [skipAccountCreation, setIsUserValid, isPasswordValid, isUserNameValid, isFullNameValid]);
useEffect(() => {
let valid = true;
setUserNameInvalidHint("");
if (checkUserName.length === 0) {
valid = null;
} else if (checkUserName.length > 32) {
valid = false;
setUserNameInvalidHint(_("User names must be shorter than 33 characters"));
} else if (reservedNames.includes(checkUserName)) {
valid = false;
setUserNameInvalidHint(_("User name must not be a reserved word"));
} else if (isUserNameWithInvalidCharacters(checkUserName)) {
valid = false;
setUserNameInvalidHint(cockpit.format(_("User name may only contain: letters from a-z, digits 0-9, dash $0, period $1, underscore $2"), "-", ".", "_"));
}
setIsUserNameValid(valid);
}, [checkUserName]);
useEffect(() => {
let valid = true;
setFullNameInvalidHint("");
if (checkFullName.length === 0) {
valid = null;
} else if (!checkFullName.match(/^[^:]*$/)) {
valid = false;
setFullNameInvalidHint(_("Full name cannot contain colon characters"));
}
setIsFullNameValid(valid);
}, [checkFullName]);
const passphraseForm = (
);
useEffect(() => {
if (skipAccountCreation) {
setAccounts({ confirmPassword, password, users: [] });
return;
}
setAccounts({
firstUser: {
confirmPassword,
gecos: fullName,
name: userName,
password,
},
});
}, [
confirmPassword,
fullName,
password,
setAccounts,
skipAccountCreation,
userName,
]);
const getValidatedVariant = (valid) => valid === null ? "default" : valid ? "success" : "error";
const userNameValidated = getValidatedVariant(isUserNameValid);
const fullNameValidated = getValidatedVariant(isFullNameValid);
const userAccountCheckbox = content => (
{
setSkipAccountCreation(!enable);
}}
body={content}
/>
);
const userFormBody = (
<>
{_("A standard user account with admin access for making system-wide changes.")}
setFullName(val)}
onBlur={async (_event) => {
if (userName.trim() !== "") {
return;
}
setGuessingUserName(true);
const generatedUserName = await guessUsernameFromFullName(_event.target.value);
setUserName(generatedUserName);
setGuessingUserName(false);
}}
validated={fullNameValidated}
/>
{fullNameValidated === "error" &&
{fullNameInvalidHint}
}
setUserName(val)}
validated={userNameValidated}
/>
{userNameValidated === "error" &&
{userNameInvalidHint}
}
{passphraseForm}
>
);
return (
{userAccountCheckbox(
!skipAccountCreation ? userFormBody : null
)}
);
};
/**
* Readonly "Root" section when root was specified via kickstart
*/
const RootAccountReadonly = ({ idPrefix, setIsRootValid }) => {
const accounts = useContext(UsersContext);
const isRootAccountEnabled = accounts.isRootEnabled;
useEffect(() => {
setIsRootValid(true);
}, [setIsRootValid]);
return (
{}}
body={isRootAccountEnabled
? (
{_("Root password has been set.")}
)
: null}
/>
);
};
const RootAccountEditable = ({
idPrefix,
setAccounts,
setIsRootValid,
}) => {
const accounts = useContext(UsersContext);
const [password, setPassword] = useState(accounts.rootPassword);
const [confirmPassword, setConfirmPassword] = useState(accounts.rootConfirmPassword);
const [isPasswordValid, setIsPasswordValid] = useState(false);
const isRootAccountEnabled = accounts.isRootEnabled;
const passwordPolicy = useContext(RuntimeContext).passwordPolicies.root;
const passwordRef = useRef();
useEffect(() => {
setIsRootValid(isPasswordValid === true || !isRootAccountEnabled);
}, [setIsRootValid, isPasswordValid, isRootAccountEnabled]);
useEffect(() => {
if (isRootAccountEnabled) {
// When the user is enabling root account, we want to focus the password field
setTimeout(() => {
if (passwordRef.current) {
passwordRef.current.focus();
}
}, 100);
}
}, [isRootAccountEnabled]);
const rootAccountCheckbox = content => (
setAccounts({ isRootEnabled: enable })}
body={content}
/>
);
const passphraseForm = (
);
useEffect(() => {
setAccounts({ rootConfirmPassword: confirmPassword, rootPassword: password });
}, [setAccounts, password, confirmPassword]);
return (
{rootAccountCheckbox(isRootAccountEnabled ? passphraseForm : null)}
);
};
const RootAccount = ({ idPrefix, setAccounts, setIsRootValid }) => {
const accounts = useContext(UsersContext);
const canModifyRootConfiguration = accounts.canModifyRootConfiguration !== false;
if (!canModifyRootConfiguration) {
return (
);
}
return (
);
};
export const Accounts = ({
dispatch,
idPrefix,
}) => {
const { setIsFormValid } = useContext(PageContext) ?? {};
const [isUserValid, setIsUserValid] = useState();
const [isRootValid, setIsRootValid] = useState();
const accounts = useContext(UsersContext);
const setAccounts = useMemo(() => args => dispatch(applyUsersPatch(args)), [dispatch]);
const [skipAccountCreation, setSkipAccountCreation] = useState(false);
const kickstartUsersReadOnly = accounts.canModifyUserConfiguration === false;
useEffect(() => {
const skipRootCreation = !accounts.isRootEnabled;
setIsFormValid(
(skipAccountCreation || isUserValid || kickstartUsersReadOnly) &&
(skipRootCreation || isRootValid) &&
!(skipRootCreation && skipAccountCreation && !kickstartUsersReadOnly)
);
}, [
accounts.isRootEnabled,
accounts.canModifyUserConfiguration,
isRootValid,
isUserValid,
setIsFormValid,
skipAccountCreation,
kickstartUsersReadOnly,
]);
// Display custom footer
const getFooter = useMemo(() => , []);
useWizardFooter(getFooter);
return (
);
};
const CustomFooter = () => {
const accounts = useContext(UsersContext);
const onNext = ({ goToNextStep }) => {
applyAccounts(accounts).then(goToNextStep);
};
const noUserAccount = (accounts.users?.length ?? 0) === 0;
return (
);
};