diff --git a/src/Components/CreateImageWizard/CreateImageWizard.tsx b/src/Components/CreateImageWizard/CreateImageWizard.tsx index 204133306..2c8ab9903 100644 --- a/src/Components/CreateImageWizard/CreateImageWizard.tsx +++ b/src/Components/CreateImageWizard/CreateImageWizard.tsx @@ -34,6 +34,7 @@ import { useFirstBootValidation, useDetailsValidation, useRegistrationValidation, + useUserValidation, } from './utilities/useValidation'; import { isAwsAccountIdValid, @@ -63,6 +64,7 @@ import { selectGcpShareMethod, selectImageTypes, addImageType, + selectUserName, } from '../../store/wizardSlice'; import { resolveRelPath } from '../../Utilities/path'; import { useFlag } from '../../Utilities/useGetEnvironment'; @@ -200,6 +202,7 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { const azureSubscriptionId = useAppSelector(selectAzureSubscriptionId); const azureResourceGroup = useAppSelector(selectAzureResourceGroup); const azureSource = useAppSelector(selectAzureSource); + const user = useAppSelector(selectUserName); // Registration const registrationValidation = useRegistrationValidation(); // Snapshots @@ -211,6 +214,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { const firstBootValidation = useFirstBootValidation(); // Details const detailsValidation = useDetailsValidation(); + // User + const userValidation = useUserValidation(); let startIndex = 1; // default index if (isEdit) { @@ -441,7 +446,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { key="wizard-users" isHidden={!isUsersEnabled} footer={ - + } > diff --git a/src/Components/CreateImageWizard/steps/Users/component/Empty.tsx b/src/Components/CreateImageWizard/steps/Users/component/Empty.tsx index cb50f2e99..454ea1f94 100644 --- a/src/Components/CreateImageWizard/steps/Users/component/Empty.tsx +++ b/src/Components/CreateImageWizard/steps/Users/component/Empty.tsx @@ -10,7 +10,10 @@ import { } from '@patternfly/react-core'; import UserIcon from '@patternfly/react-icons/dist/esm/icons/user-icon'; -const EmptyUserState = () => { +interface EmptyUserStateProps { + onAddUserClick: () => void; +} +const EmptyUserState = ({ onAddUserClick }: EmptyUserStateProps) => { return ( { headingLevel="h4" /> - ); }; + export default EmptyUserState; diff --git a/src/Components/CreateImageWizard/steps/Users/component/UserInfo.tsx b/src/Components/CreateImageWizard/steps/Users/component/UserInfo.tsx new file mode 100644 index 000000000..9014029b3 --- /dev/null +++ b/src/Components/CreateImageWizard/steps/Users/component/UserInfo.tsx @@ -0,0 +1,120 @@ +import React, { useState } from 'react'; + +import { + Checkbox, + Form, + FormGroup, + FormHelperText, + HelperText, + HelperTextItem, + Text, + TextVariants, +} from '@patternfly/react-core'; + +import { useAppDispatch, useAppSelector } from '../../../../../store/hooks'; +import { + selectUserName, + selectUserPassword, + selectConfirmUserPassword, + selectUserAdministrator, + setUserName, + setUserPassword, + setConfirmUserPassword, + changeUserAdministrator, +} from '../../../../../store/wizardSlice'; +import { useUserValidation } from '../../../utilities/useValidation'; +import { HookValidatedInput } from '../../../ValidatedTextInput'; + +const UserInfo = () => { + const dispatch = useAppDispatch(); + const userName = useAppSelector(selectUserName); + const userPassword = useAppSelector(selectUserPassword); + const confirmUserPassword = useAppSelector(selectConfirmUserPassword); + const userAdministrator = useAppSelector(selectUserAdministrator); + + const handleNameChange = ( + _event: React.FormEvent, + name: string + ) => { + dispatch(setUserName(name)); + }; + const handlePasswordChange = ( + _event: React.FormEvent, + password: string + ) => { + dispatch(setUserPassword(password)); + }; + const handleConfirmPasswordChange = ( + _event: React.FormEvent, + confirm: string + ) => { + dispatch(setConfirmUserPassword(confirm)); + }; + const handleCheckboxChange = ( + _event: React.FormEvent, + userAdministrator: boolean + ) => { + dispatch(changeUserAdministrator(userAdministrator)); + }; + + const stepValidation = useUserValidation(); + return ( +
+ + + + + + Can only contain letters, numbers, hyphens (-), and + underscores(_). + + + + + + + + + + + + + + + +
+ ); +}; +export default UserInfo; diff --git a/src/Components/CreateImageWizard/steps/Users/index.tsx b/src/Components/CreateImageWizard/steps/Users/index.tsx index c2a62a234..e37e684a6 100644 --- a/src/Components/CreateImageWizard/steps/Users/index.tsx +++ b/src/Components/CreateImageWizard/steps/Users/index.tsx @@ -1,17 +1,34 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Form, Text, Title } from '@patternfly/react-core'; import EmptyUserState from './component/Empty'; +import UserInfo from './component/UserInfo'; + +import { useAppSelector } from '../../../../store/hooks'; +import { selectUserName } from '../../../../store/wizardSlice'; const UsersStep = () => { + const userName = useAppSelector(selectUserName); + const [showUserInfo, setShowUserInfo] = useState(false); + + const handleAddUserClick = () => { + setShowUserInfo(true); + }; + return (
Users Add a user to your image. - + {userName ? ( + + ) : !showUserInfo ? ( + + ) : ( + + )} ); }; diff --git a/src/Components/CreateImageWizard/utilities/useValidation.tsx b/src/Components/CreateImageWizard/utilities/useValidation.tsx index 03f1dc4c4..a7da2a798 100644 --- a/src/Components/CreateImageWizard/utilities/useValidation.tsx +++ b/src/Components/CreateImageWizard/utilities/useValidation.tsx @@ -18,6 +18,9 @@ import { selectUseLatest, selectActivationKey, selectRegistrationType, + selectUserName, + selectUserPassword, + selectConfirmUserPassword, } from '../../../store/wizardSlice'; import { getDuplicateMountPoints, @@ -25,6 +28,8 @@ import { isBlueprintDescriptionValid, isMountpointMinSizeValid, isSnapshotValid, + isPasswordValid, + isUserNameValid, } from '../validators'; export type StepValidation = { @@ -133,6 +138,24 @@ export function useFirstBootValidation(): StepValidation { }; } +export function useUserValidation(): StepValidation { + const userName = useAppSelector(selectUserName); + const password = useAppSelector(selectUserPassword); + const confirmPassword = useAppSelector(selectConfirmUserPassword); + const userNameValid = isUserNameValid(userName); + const passwordValid = isPasswordValid(password, confirmPassword); + + if (!userNameValid || !passwordValid) { + return { + errors: { + userName: 'Invalid user name', + password: 'Invalid password name', + }, + disabledNext: true, + }; + } + return { errors: {}, disabledNext: false }; +} export function useDetailsValidation(): StepValidation { const name = useAppSelector(selectBlueprintName); const description = useAppSelector(selectBlueprintDescription); diff --git a/src/Components/CreateImageWizard/validators.ts b/src/Components/CreateImageWizard/validators.ts index 3cdd0754a..2b6b84c61 100644 --- a/src/Components/CreateImageWizard/validators.ts +++ b/src/Components/CreateImageWizard/validators.ts @@ -58,6 +58,42 @@ export const isSnapshotValid = (dateString: string) => { return !isNaN(date.getTime()) && isSnapshotDateValid(date); }; +export const isUserNameValid = (userName: string) => { + const isLengthValid = + userName !== undefined && userName.length >= 2 && userName.length <= 32; + const isPatternValid = /^[a-zA-Z0-9_.][a-zA-Z0-9_.-]*[a-zA-Z0-9_.$-]?$/.test( + userName + ); + //const isNonNumericValid = + // version === '4.1.5.1-25.el7' ? !/^\d+$/.test(userName) : true; + + return isLengthValid && isPatternValid; +}; + +export const isPasswordValid = ( + password: string, + confirmPassword: string +): boolean => { + const isLengthValid = password.length >= 6; + const containsUppercase = /[A-Z]/.test(password); + const containsLowercase = /[a-z]/.test(password); + const containsNumber = /\d/.test(password); + + // Ensure no special characters for plaintext passwords + const isPlainText = /^[A-Za-z0-9]*$/.test(password); + + const classCount = [ + containsUppercase, + containsLowercase, + containsNumber, + ].filter(Boolean).length; + + const isClassValid = classCount >= 3; + const passwordsMatch = password === confirmPassword; + + return isLengthValid && isClassValid && passwordsMatch && isPlainText; +}; + export const isBlueprintDescriptionValid = (blueprintDescription: string) => { return blueprintDescription.length <= 250; }; diff --git a/src/store/wizardSlice.ts b/src/store/wizardSlice.ts index 1dc7780fb..3a872ce3e 100644 --- a/src/store/wizardSlice.ts +++ b/src/store/wizardSlice.ts @@ -87,6 +87,13 @@ export type wizardState = { useLatest: boolean; snapshotDate: string; }; + userInfo: { + userName: string; + password: string; + confirmPassword: string; + sshKey: string; + administrator: boolean; + }; firstBoot: { script: string; }; @@ -179,6 +186,13 @@ export const initialState: wizardState = { blueprintDescription: '', }, firstBoot: { script: '' }, + userInfo: { + userName: '', + password: '', + confirmPassword: '', + sshKey: '', + administrator: false, + }, }; export const selectServerUrl = (state: RootState) => { @@ -336,6 +350,25 @@ export const selectFirstBootScript = (state: RootState) => { return state.wizard.firstBoot?.script; }; +export const selectUserName = (state: RootState) => { + return state.wizard.userInfo?.userName; +}; + +export const selectUserPassword = (state: RootState) => { + return state.wizard.userInfo?.password; +}; + +export const selectConfirmUserPassword = (state: RootState) => { + return state.wizard.userInfo?.confirmPassword; +}; +export const selectUserSshKey = (state: RootState) => { + return state.wizard.userInfo?.sshKey; +}; + +export const selectUserAdministrator = (state: RootState) => { + return state.wizard.userInfo?.administrator; +}; + export const wizardSlice = createSlice({ name: 'wizard', initialState, @@ -656,6 +689,22 @@ export const wizardSlice = createSlice({ setFirstBootScript: (state, action: PayloadAction) => { state.firstBoot.script = action.payload; }, + setUserName: (state, action: PayloadAction) => { + state.userInfo.userName = action.payload; + }, + setUserPassword: (state, action: PayloadAction) => { + state.userInfo.password = action.payload; + }, + + setConfirmUserPassword: (state, action: PayloadAction) => { + state.userInfo.confirmPassword = action.payload; + }, + setUserSshKey: (state, action: PayloadAction) => { + state.userInfo.sshKey = action.payload; + }, + changeUserAdministrator: (state, action: PayloadAction) => { + state.userInfo.administrator = action.payload; + }, changeEnabledServices: (state, action: PayloadAction) => { state.services.enabled = action.payload; }, @@ -722,6 +771,11 @@ export const { changeBlueprintDescription, loadWizardState, setFirstBootScript, + setUserName, + setUserPassword, + setConfirmUserPassword, + setUserSshKey, + changeUserAdministrator, changeEnabledServices, changeMaskedServices, changeDisabledServices, diff --git a/src/test/Components/CreateImageWizard/steps/Users/Users.test.tsx b/src/test/Components/CreateImageWizard/steps/Users/Users.test.tsx new file mode 100644 index 000000000..e69de29bb