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