/*
* Copyright (C) 2023 Red Hat, Inc.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with This program; If not, see .
*/
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 { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
import { useWizardFooter } from "@patternfly/react-core/dist/esm/components/Wizard/index.js";
import {
guessUsernameFromFullName,
} from "../../apis/users.js";
import {
setUsersAction,
} from "../../actions/users-actions.js";
import {
applyAccounts,
} from "../../helpers/users.js";
import { RuntimeContext, UsersContext } from "../../contexts/Common.jsx";
import { AnacondaWizardFooter } from "../AnacondaWizardFooter.jsx";
import { PasswordFormFields, ruleLength } from "../Password.jsx";
import "./Accounts.scss";
const _ = cockpit.gettext;
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,
}) => {
const accounts = useContext(UsersContext);
const [fullName, setFullName] = useState(accounts.fullName);
const [checkFullName, setCheckFullName] = useState(accounts.fullName);
const [fullNameInvalidHint, setFullNameInvalidHint] = useState("");
const [isFullNameValid, setIsFullNameValid] = useState(null);
const [userName, setUserName] = useState(accounts.userName);
const [checkUserName, setCheckUserName] = useState(accounts.userName);
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);
const [skipAccountCreation, setSkipAccountCreation] = useState(accounts.skipAccountCreation);
useEffect(() => {
debounce(300, () => setCheckUserName(userName))();
}, [userName, setCheckUserName]);
useEffect(() => {
debounce(300, () => setCheckFullName(fullName))();
}, [fullName, setCheckFullName]);
useEffect(() => {
setIsUserValid(
(isPasswordValid !== false && isUserNameValid !== false && 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(() => {
setAccounts({ confirmPassword, fullName, password, skipAccountCreation, userName });
}, [
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
)}
);
};
const RootAccount = ({
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 || !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)}
);
};
export const Accounts = ({
dispatch,
idPrefix,
setIsFormValid,
}) => {
const [isUserValid, setIsUserValid] = useState();
const [isRootValid, setIsRootValid] = useState();
const accounts = useContext(UsersContext);
const setAccounts = useMemo(() => args => dispatch(setUsersAction(args)), [dispatch]);
useEffect(() => {
const skipRootCreation = !accounts.isRootEnabled;
const skipAccountCreation = accounts.skipAccountCreation;
setIsFormValid(
(skipAccountCreation || isUserValid) &&
(skipRootCreation || isRootValid) &&
!(skipRootCreation && skipAccountCreation)
);
}, [
accounts.isRootEnabled,
accounts.skipAccountCreation,
isRootValid,
isUserValid,
setIsFormValid,
]);
// Display custom footer
const getFooter = useMemo(() => , []);
useWizardFooter(getFooter);
return (
);
};
const CustomFooter = () => {
const accounts = useContext(UsersContext);
const onNext = ({ goToNextStep }) => {
applyAccounts(accounts).then(goToNextStep);
};
return (
);
};