/* * 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 ( ); };