feat: Added password reset as a route to manage initial login case (#4744)

* feat: Added password reset as a route to manage initial login case

Signed-off-by: Hrishav <hrishav.kumar@harness.io>

* feat: Fixed deepspan issue

Signed-off-by: Hrishav <hrishav.kumar@harness.io>

* fix: Updated API response in front-end

Signed-off-by: Hrishav <hrishav.kumar@harness.io>

* chore: addressed review comment

Signed-off-by: Hrishav <hrishav.kumar@harness.io>

---------

Signed-off-by: Hrishav <hrishav.kumar@harness.io>
This commit is contained in:
Hrishav 2024-07-05 12:37:36 +05:30 committed by GitHub
parent 00f0bd7366
commit fb46bb9334
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 347 additions and 114 deletions

View File

@ -442,8 +442,10 @@ func UpdatePassword(service services.ApplicationService) gin.HandlerFunc {
log.Info(err)
if strings.Contains(err.Error(), "old and new passwords can't be same") {
c.JSON(utils.ErrorStatusCodes[utils.ErrOldPassword], presenter.CreateErrorResponse(utils.ErrOldPassword))
} else if strings.Contains(err.Error(), "invalid credentials") {
c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidCredentials], presenter.CreateErrorResponse(utils.ErrInvalidCredentials))
} else {
c.JSON(utils.ErrorStatusCodes[utils.ErrInvalidRequest], presenter.CreateErrorResponse(utils.ErrInvalidRequest))
c.JSON(utils.ErrorStatusCodes[utils.ErrServerError], presenter.CreateErrorResponse(utils.ErrServerError))
}
return
}

View File

@ -187,7 +187,7 @@ func (r repository) UpdatePassword(userPassword *entities.UserPassword, isAdminB
if isAdminBeingReset {
err := bcrypt.CompareHashAndPassword([]byte(result.Password), []byte(userPassword.OldPassword))
if err != nil {
return err
return fmt.Errorf("invalid credentials")
}
// check if the new pwd is same as old pwd, if yes return err
err = bcrypt.CompareHashAndPassword([]byte(result.Password), []byte(userPassword.NewPassword))

View File

@ -3,6 +3,9 @@
// Please do not modify this code directly.
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
import type { ResponseMessageResponse } from '../schemas/ResponseMessageResponse';
import type { ResponseErrOldPassword } from '../schemas/ResponseErrOldPassword';
import type { ResponseErrInvalidCredentials } from '../schemas/ResponseErrInvalidCredentials';
import { fetcher, FetcherOptions } from 'services/fetcher';
export type UpdatePasswordRequestBody = {
@ -11,11 +14,9 @@ export type UpdatePasswordRequestBody = {
username: string;
};
export type UpdatePasswordOkResponse = {
message?: string;
};
export type UpdatePasswordOkResponse = ResponseMessageResponse;
export type UpdatePasswordErrorResponse = unknown;
export type UpdatePasswordErrorResponse = ResponseErrOldPassword | ResponseErrInvalidCredentials;
export interface UpdatePasswordProps extends Omit<FetcherOptions<unknown, UpdatePasswordRequestBody>, 'url'> {
body: UpdatePasswordRequestBody;

View File

@ -223,5 +223,8 @@ export type { LogoutResponse } from './schemas/LogoutResponse';
export type { Project } from './schemas/Project';
export type { ProjectMember } from './schemas/ProjectMember';
export type { RemoveApiTokenResponse } from './schemas/RemoveApiTokenResponse';
export type { ResponseErrInvalidCredentials } from './schemas/ResponseErrInvalidCredentials';
export type { ResponseErrOldPassword } from './schemas/ResponseErrOldPassword';
export type { ResponseMessageResponse } from './schemas/ResponseMessageResponse';
export type { User } from './schemas/User';
export type { Users } from './schemas/Users';

View File

@ -0,0 +1,14 @@
/* eslint-disable */
// This code is autogenerated using @harnessio/oats-cli.
// Please do not modify this code directly.
export interface ResponseErrInvalidCredentials {
/**
* @example "The old and new passwords can't be same"
*/
error?: string;
/**
* @example "The old and new passwords can't be same"
*/
errorDescription?: string;
}

View File

@ -0,0 +1,14 @@
/* eslint-disable */
// This code is autogenerated using @harnessio/oats-cli.
// Please do not modify this code directly.
export interface ResponseErrOldPassword {
/**
* @example "The old and new passwords can't be same"
*/
error?: string;
/**
* @example "The old and new passwords can't be same"
*/
errorDescription?: string;
}

View File

@ -0,0 +1,7 @@
/* eslint-disable */
// This code is autogenerated using @harnessio/oats-cli.
// Please do not modify this code directly.
export interface ResponseMessageResponse {
message?: string;
}

View File

@ -8,7 +8,7 @@ interface PasswordInputProps {
disabled?: boolean;
placeholder?: string;
name: string;
label: string;
label: string | React.ReactElement;
}
const PasswordInput = (props: PasswordInputProps): React.ReactElement => {
@ -22,10 +22,12 @@ const PasswordInput = (props: PasswordInputProps): React.ReactElement => {
return (
<Layout.Vertical className={style.fieldContainer}>
{label && (
{label && typeof label === 'string' ? (
<Text font={{ variation: FontVariation.BODY, weight: 'semi-bold' }} color={Color.GREY_600}>
{label}
</Text>
) : (
label
)}
<div className={style.inputContainer}>
<FormInput.Text

View File

@ -1,24 +1,20 @@
import React from 'react';
import { useToaster } from '@harnessio/uicore';
import { useHistory } from 'react-router-dom';
import { useUpdatePasswordMutation } from '@api/auth';
import AccountPasswordChangeView from '@views/AccountPasswordChange';
import { useLogout, useRouteWithBaseUrl } from '@hooks';
import { useLogout } from '@hooks';
import { useStrings } from '@strings';
import { setUserDetails } from '@utils';
interface AccountPasswordChangeViewProps {
handleClose: () => void;
username: string | undefined;
initialMode?: boolean;
}
export default function AccountPasswordChangeController(props: AccountPasswordChangeViewProps): React.ReactElement {
const { handleClose, username, initialMode } = props;
const { handleClose, username } = props;
const { showSuccess } = useToaster();
const { getString } = useStrings();
const history = useHistory();
const paths = useRouteWithBaseUrl();
const { forceLogout } = useLogout();
const { mutate: updatePasswordMutation, isLoading } = useUpdatePasswordMutation(
@ -26,12 +22,8 @@ export default function AccountPasswordChangeController(props: AccountPasswordCh
{
onSuccess: data => {
setUserDetails({ isInitialLogin: false });
if (initialMode) {
history.push(paths.toDashboard());
} else {
showSuccess(`${data.message}, ${getString('loginToContinue')}`);
forceLogout();
}
showSuccess(`${data.message}, ${getString('loginToContinue')}`);
forceLogout();
}
}
);
@ -42,7 +34,6 @@ export default function AccountPasswordChangeController(props: AccountPasswordCh
updatePasswordMutation={updatePasswordMutation}
updatePasswordMutationLoading={isLoading}
username={username}
initialMode={initialMode}
/>
);
}

View File

@ -4,7 +4,7 @@ import { useToaster } from '@harnessio/uicore';
import jwtDecode from 'jwt-decode';
import LoginPageView from '@views/Login';
import { useLoginMutation, useGetCapabilitiesQuery, useGetUserQuery } from '@api/auth';
import { getUserDetails, setUserDetails } from '@utils';
import { getUserDetails, setUserDetails, toTitleCase } from '@utils';
import { normalizePath } from '@routes/RouteDefinitions';
import type { DecodedTokenType, PermissionGroup } from '@models';
import { useSearchParams } from '@hooks';
@ -37,7 +37,13 @@ const LoginController: React.FC = () => {
const { isLoading, mutate: handleLogin } = useLoginMutation(
{},
{
onError: err => showError(err.error),
onError: err =>
showError(
toTitleCase({
separator: '_',
text: err.error ?? ''
})
),
onSuccess: response => {
if (response.accessToken) {
setUserDetails(response);
@ -60,9 +66,13 @@ const LoginController: React.FC = () => {
setUserDetails({
isInitialLogin: response.isInitialLogin
});
history.push(
normalizePath(`/account/${userDetails.accountID}/project/${userDetails.projectID ?? ''}/dashboard`)
);
if (response.isInitialLogin) {
history.push(`/account/${userDetails.accountID}/settings/password-reset`);
} else {
history.push(
normalizePath(`/account/${userDetails.accountID}/project/${userDetails.projectID ?? ''}/dashboard`)
);
}
}
}
);

View File

@ -1,16 +1,14 @@
import React from 'react';
import { useToaster } from '@harnessio/uicore';
import { getChaosHubStats, getExperimentStats, getInfraStats, listExperiment } from '@api/core';
import { getScope, getUserDetails } from '@utils';
import { getScope } from '@utils';
import OverviewView from '@views/Overview';
import { generateExperimentDashboardTableContent } from '@controllers/ExperimentDashboardV2/helpers';
import type { ExperimentDashboardTableProps } from '@controllers/ExperimentDashboardV2';
import { useGetUserQuery } from '@api/auth';
export default function OverviewController(): React.ReactElement {
const scope = getScope();
const { showError } = useToaster();
const userDetails = getUserDetails();
const { data: chaosHubStats, loading: loadingChaosHubStats } = getChaosHubStats({
...scope
@ -37,15 +35,6 @@ export default function OverviewController(): React.ReactElement {
}
});
const { data: currentUserData, isLoading: getUserLoading } = useGetUserQuery(
{
user_id: userDetails.accountID
},
{
enabled: !!userDetails.accountID
}
);
const experiments = experimentRunData?.listExperiment.experiments;
const experimentDashboardTableData: ExperimentDashboardTableProps | undefined = experiments && {
@ -58,10 +47,8 @@ export default function OverviewController(): React.ReactElement {
chaosHubStats: loadingChaosHubStats,
infraStats: loadingInfraStats,
experimentStats: loadingExperimentStats,
recentExperimentsTable: loadingRecentExperimentsTable,
getUser: getUserLoading
recentExperimentsTable: loadingRecentExperimentsTable
}}
currentUserData={currentUserData}
chaosHubStats={chaosHubStats?.getChaosHubStats}
infraStats={infraStats?.getInfraStats}
experimentStats={experimentStats?.getExperimentStats}

View File

@ -0,0 +1,47 @@
import React from 'react';
import { useToaster } from '@harnessio/uicore';
import { useHistory } from 'react-router-dom';
import PasswordResetView from '@views/PasswordReset';
import { useGetUserQuery, useUpdatePasswordMutation } from '@api/auth';
import { getUserDetails, setUserDetails } from '@utils';
import { normalizePath } from '@routes/RouteDefinitions';
const PasswordResetController = (): React.ReactElement => {
const { accountID, projectID } = getUserDetails();
const { showSuccess, showError } = useToaster();
const history = useHistory();
const { data: currentUserData, isLoading: getUserLoading } = useGetUserQuery(
{
user_id: accountID
},
{
enabled: !!accountID
}
);
const { mutate: updatePasswordMutation, isLoading: updatePasswordLoading } = useUpdatePasswordMutation(
{},
{
onSuccess: data => {
setUserDetails({ isInitialLogin: false });
showSuccess(`${data.message}`);
history.push(normalizePath(`/account/${accountID}/project/${projectID}/dashboard`));
},
onError: err => showError(err.errorDescription)
}
);
return (
<PasswordResetView
currentUserData={currentUserData}
updatePasswordMutation={updatePasswordMutation}
loading={{
getUser: getUserLoading,
updatePassword: updatePasswordLoading
}}
/>
);
};
export default PasswordResetController;

View File

@ -0,0 +1,3 @@
import PasswordResetController from './PasswordReset';
export default PasswordResetController;

View File

@ -24,6 +24,7 @@ export interface UseRouteDefinitionsProps {
toKubernetesChaosInfrastructures(params: { environmentID: string }): string;
toKubernetesChaosInfrastructureDetails(params: { chaosInfrastructureID: string; environmentID: string }): string;
toAccountSettingsOverview(): string;
toPasswordReset(): string;
toProjectSetup(): string;
toProjectMembers(): string;
toImageRegistry(): string;
@ -60,6 +61,7 @@ export const paths: UseRouteDefinitionsProps = {
`/environments/${environmentID}/kubernetes/${chaosInfrastructureID}`,
// Account Scoped Routes
toAccountSettingsOverview: () => '/settings/overview',
toPasswordReset: () => '/settings/password-reset',
// Project Setup Routes
toProjectSetup: () => '/setup',
toProjectMembers: () => '/setup/members',

View File

@ -25,6 +25,7 @@ import AccountSettingsController from '@controllers/AccountSettings';
import ProjectMembersView from '@views/ProjectMembers';
import ChaosProbesController from '@controllers/ChaosProbes';
import ChaosProbeController from '@controllers/ChaosProbe';
import PasswordResetController from '@controllers/PasswordReset';
const experimentID = ':experimentID';
const runID = ':runID';
@ -45,14 +46,14 @@ export function RoutesWithAuthentication(): React.ReactElement {
const history = useHistory();
const { forceLogout } = useLogout();
const { accessToken: token, isInitialLogin } = getUserDetails();
const { accessToken: token, isInitialLogin, accountID } = getUserDetails();
useEffect(() => {
if (!token || !isUserAuthenticated()) {
forceLogout();
}
if (isInitialLogin) {
history.push(projectRenderPaths.toDashboard());
history.push(`/account/${accountID}/settings/password-reset`);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token, isInitialLogin]);
@ -61,7 +62,10 @@ export function RoutesWithAuthentication(): React.ReactElement {
<Switch>
<Redirect exact from={accountMatchPaths.toRoot()} to={accountRenderPaths.toAccountSettingsOverview()} />
<Redirect exact from={projectMatchPaths.toRoot()} to={projectRenderPaths.toDashboard()} />
<Route exact path={accountMatchPaths.toAccountSettingsOverview()} component={AccountSettingsController} />
{/* Account */}
<Route exact path={accountRenderPaths.toAccountSettingsOverview()} component={AccountSettingsController} />
<Route exact path={accountRenderPaths.toPasswordReset()} component={PasswordResetController} />
{/* Dashboard */}
<Route exact path={projectMatchPaths.toDashboard()} component={OverviewController} />
{/* Chaos Experiments */}
<Route exact path={projectMatchPaths.toExperiments()} component={ExperimentDashboardV2Controller} />

View File

@ -1,25 +1,25 @@
import { FontVariation } from '@harnessio/design-system';
import { Button, ButtonVariation, Container, FormInput, Layout, Text, useToaster } from '@harnessio/uicore';
import { Button, ButtonVariation, Container, Layout, Text, useToaster } from '@harnessio/uicore';
import React from 'react';
import { Icon } from '@harnessio/icons';
import { Form, Formik } from 'formik';
import * as Yup from 'yup';
import type { UseMutateFunction } from '@tanstack/react-query';
import { useStrings } from '@strings';
import type { UpdatePasswordMutationProps, UpdatePasswordOkResponse } from '@api/auth';
import type { ResponseMessageResponse, UpdatePasswordErrorResponse, UpdatePasswordMutationProps } from '@api/auth';
import { PASSWORD_REGEX } from '@constants/validation';
import PasswordInput from '@components/PasswordInput';
interface AccountPasswordChangeViewProps {
handleClose: () => void;
username: string | undefined;
updatePasswordMutation: UseMutateFunction<
UpdatePasswordOkResponse,
unknown,
ResponseMessageResponse,
UpdatePasswordErrorResponse,
UpdatePasswordMutationProps<never>,
unknown
>;
updatePasswordMutationLoading: boolean;
initialMode?: boolean;
}
interface AccountPasswordChangeFormProps {
oldPassword: string;
@ -28,7 +28,7 @@ interface AccountPasswordChangeFormProps {
}
export default function AccountPasswordChangeView(props: AccountPasswordChangeViewProps): React.ReactElement {
const { handleClose, updatePasswordMutation, updatePasswordMutationLoading, username, initialMode } = props;
const { handleClose, updatePasswordMutation, updatePasswordMutationLoading, username } = props;
const { getString } = useStrings();
const { showError } = useToaster();
@ -59,7 +59,7 @@ export default function AccountPasswordChangeView(props: AccountPasswordChangeVi
}
},
{
onError: () => showError(getString('passwordsDoNotMatch')),
onError: err => showError(err.errorDescription),
onSuccess: () => handleClose()
}
);
@ -69,7 +69,7 @@ export default function AccountPasswordChangeView(props: AccountPasswordChangeVi
<Layout.Vertical padding="medium" style={{ gap: '1rem' }}>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'space-between' }}>
<Text font={{ variation: FontVariation.H4 }}>{getString('updatePassword')}</Text>
{!initialMode && <Icon name="cross" style={{ cursor: 'pointer' }} size={18} onClick={handleClose} />}
<Icon name="cross" style={{ cursor: 'pointer' }} size={18} onClick={handleClose} />
</Layout.Horizontal>
<Container>
<Formik<AccountPasswordChangeFormProps>
@ -95,29 +95,26 @@ export default function AccountPasswordChangeView(props: AccountPasswordChangeVi
return (
<Form style={{ height: '100%' }}>
<Layout.Vertical style={{ gap: '2rem' }}>
<Container>
<FormInput.Text
<Layout.Vertical width="100%" style={{ gap: '0.5rem' }}>
<PasswordInput
name="oldPassword"
placeholder={getString('oldPassword')}
inputGroup={{ type: 'password' }}
label={<Text font={{ variation: FontVariation.FORM_LABEL }}>{getString('oldPassword')}</Text>}
/>
<FormInput.Text
<PasswordInput
name="newPassword"
placeholder={getString('newPassword')}
inputGroup={{ type: 'password' }}
label={<Text font={{ variation: FontVariation.FORM_LABEL }}>{getString('newPassword')}</Text>}
/>
<FormInput.Text
<PasswordInput
name="reEnterNewPassword"
placeholder={getString('reEnterNewPassword')}
inputGroup={{ type: 'password' }}
label={
<Text font={{ variation: FontVariation.FORM_LABEL }}>{getString('reEnterNewPassword')}</Text>
}
/>
</Container>
<Layout.Horizontal style={{ gap: '1rem' }}>
</Layout.Vertical>
<Layout.Horizontal style={{ gap: '1rem' }} width="100%">
<Button
type="submit"
variation={ButtonVariation.PRIMARY}
@ -126,9 +123,7 @@ export default function AccountPasswordChangeView(props: AccountPasswordChangeVi
disabled={updatePasswordMutationLoading || isSubmitButtonDisabled(formikProps.values)}
style={{ minWidth: '90px' }}
/>
{!initialMode && (
<Button variation={ButtonVariation.TERTIARY} text={getString('cancel')} onClick={handleClose} />
)}
<Button variation={ButtonVariation.TERTIARY} text={getString('cancel')} onClick={handleClose} />
</Layout.Horizontal>
</Layout.Vertical>
</Form>

View File

@ -1,6 +1,6 @@
import { IDialogProps, Dialog as BluePrintDialog } from '@blueprintjs/core';
import { IDialogProps } from '@blueprintjs/core';
import { Color, FontVariation } from '@harnessio/design-system';
import { Button, ButtonVariation, Layout, Text, useToggleOpen, Dialog } from '@harnessio/uicore';
import { Button, ButtonVariation, Layout, Text, Dialog } from '@harnessio/uicore';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { isEmpty } from 'lodash-es';
@ -17,8 +17,6 @@ import NewExperimentButton from '@components/NewExperimentButton';
import RbacButton from '@components/RbacButton';
import { PermissionGroup } from '@models';
import { getUserDetails } from '@utils';
import AccountPasswordChangeController from '@controllers/AccountPasswordChange';
import { User } from '@api/auth';
import TotalChaosHubsCard from './TotalChaosHubsCard';
import TotalExperimentCard from './TotalExperimentCard';
import TotalInfrastructureCard from './TotalInfrastructureCard';
@ -34,9 +32,7 @@ interface OverviewViewProps {
infraStats: boolean;
experimentStats: boolean;
recentExperimentsTable: boolean;
getUser: boolean;
};
currentUserData: User | undefined;
}
export default function OverviewView({
@ -45,26 +41,17 @@ export default function OverviewView({
infraStats,
experimentDashboardTableData,
experimentStats,
refetchExperiments,
currentUserData
refetchExperiments
}: OverviewViewProps & RefetchExperiments): React.ReactElement {
const { getString } = useStrings();
const paths = useRouteWithBaseUrl();
const history = useHistory();
const {
isOpen: isPasswordResetModalOpen,
close: closePasswordResetModal,
open: openPasswordResetModal
} = useToggleOpen();
const [isEnableChaosModalOpen, setIsEnableChaosModalOpen] = React.useState(false);
const userDetails = getUserDetails();
React.useEffect(() => {
if (userDetails?.isInitialLogin) {
openPasswordResetModal();
}
if (infraStats?.totalInfrastructures === 0 && !isPasswordResetModalOpen) setIsEnableChaosModalOpen(true);
if (infraStats?.totalInfrastructures === 0) setIsEnableChaosModalOpen(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [infraStats?.totalInfrastructures, userDetails?.isInitialLogin]);
@ -100,21 +87,6 @@ export default function OverviewView({
color={Color.PRIMARY_BG}
spacing="xlarge"
>
<BluePrintDialog
isOpen={isPasswordResetModalOpen}
canOutsideClickClose={false}
canEscapeKeyClose={false}
onClose={closePasswordResetModal}
style={{
paddingBottom: 0
}}
>
<AccountPasswordChangeController
handleClose={closePasswordResetModal}
username={currentUserData?.username}
initialMode={true}
/>
</BluePrintDialog>
<Layout.Vertical spacing="medium">
<Text font={{ variation: FontVariation.H5 }}>{getString('atAGlance')}</Text>
<Layout.Horizontal spacing="medium">

View File

@ -0,0 +1,4 @@
.formContainer {
border-radius: 4px;
transform: translateY(-82px);
}

View File

@ -0,0 +1,9 @@
declare namespace PasswordResetModuleScssNamespace {
export interface IPasswordResetModuleScss {
formContainer: string;
}
}
declare const PasswordResetModuleScssModule: PasswordResetModuleScssNamespace.IPasswordResetModuleScss;
export = PasswordResetModuleScssModule;

View File

@ -0,0 +1,128 @@
import { Color, FontVariation } from '@harnessio/design-system';
import { Button, ButtonVariation, Container, Layout, Text } from '@harnessio/uicore';
import React from 'react';
import { Form, Formik } from 'formik';
import * as Yup from 'yup';
import { Icon } from '@harnessio/icons';
import { UseMutateFunction } from '@tanstack/react-query';
import { useStrings } from '@strings';
import { PASSWORD_REGEX } from '@constants/validation';
import { User, UpdatePasswordMutationProps, ResponseMessageResponse, UpdatePasswordErrorResponse } from '@api/auth';
import PasswordInput from '@components/PasswordInput';
import css from './PasswordReset.module.scss';
interface PasswordResetViewProps {
currentUserData: User | undefined;
updatePasswordMutation: UseMutateFunction<
ResponseMessageResponse,
UpdatePasswordErrorResponse,
UpdatePasswordMutationProps<never>,
unknown
>;
loading: {
getUser: boolean;
updatePassword: boolean;
};
}
interface AccountPasswordChangeFormProps {
oldPassword: string;
newPassword: string;
reEnterNewPassword: string;
}
const PasswordResetView = (props: PasswordResetViewProps): React.ReactElement => {
const { currentUserData, updatePasswordMutation, loading } = props;
const { getString } = useStrings();
function handleSubmit(values: AccountPasswordChangeFormProps): void {
updatePasswordMutation({
body: {
username: currentUserData?.username ?? '',
oldPassword: values.oldPassword,
newPassword: values.newPassword
}
});
}
return (
<Layout.Vertical width="100%" height="100vh" background={Color.PRIMARY_6}>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'center' }} padding="medium">
<Icon name="chaos-litmuschaos" size={50} />
</Layout.Horizontal>
<Container height="calc(100% - 82px)" flex={{ align: 'center-center' }}>
<Layout.Vertical
padding="medium"
style={{ gap: '1rem' }}
background={Color.WHITE}
width={500}
className={css.formContainer}
>
<Text font={{ variation: FontVariation.H4 }}>{getString('updatePassword')}</Text>
<Container>
<Formik<AccountPasswordChangeFormProps>
initialValues={{
oldPassword: '',
newPassword: '',
reEnterNewPassword: ''
}}
onSubmit={handleSubmit}
validationSchema={Yup.object().shape({
oldPassword: Yup.string().required(getString('enterOldPassword')),
newPassword: Yup.string()
.required(getString('enterNewPassword'))
.min(8, getString('fieldMinLength', { length: 8 }))
.max(16, getString('fieldMaxLength', { length: 16 }))
.matches(PASSWORD_REGEX, getString('passwordValidation')),
reEnterNewPassword: Yup.string()
.required(getString('reEnterNewPassword'))
.oneOf([Yup.ref('newPassword'), null], getString('passwordsDoNotMatch'))
})}
>
{formikProps => {
return (
<Form style={{ height: '100%' }}>
<Layout.Vertical style={{ gap: '2rem' }}>
<Layout.Vertical width="100%" style={{ gap: '0.5rem' }}>
<PasswordInput
name="oldPassword"
placeholder={getString('oldPassword')}
label={<Text font={{ variation: FontVariation.FORM_LABEL }}>{getString('oldPassword')}</Text>}
/>
<PasswordInput
name="newPassword"
placeholder={getString('newPassword')}
label={<Text font={{ variation: FontVariation.FORM_LABEL }}>{getString('newPassword')}</Text>}
/>
<PasswordInput
name="reEnterNewPassword"
placeholder={getString('reEnterNewPassword')}
label={
<Text font={{ variation: FontVariation.FORM_LABEL }}>
{getString('reEnterNewPassword')}
</Text>
}
/>
</Layout.Vertical>
<Layout.Horizontal width="100%" flex={{ alignItems: 'center', justifyContent: 'flex-start' }}>
<Button
type="submit"
variation={ButtonVariation.PRIMARY}
text={loading.updatePassword ? <Icon name="loading" size={16} /> : getString('confirm')}
disabled={loading.updatePassword || Object.keys(formikProps.errors).length > 0}
style={{ minWidth: '90px' }}
/>
</Layout.Horizontal>
</Layout.Vertical>
</Form>
);
}}
</Formik>
</Container>
</Layout.Vertical>
</Container>
</Layout.Vertical>
);
};
export default PasswordResetView;

View File

@ -0,0 +1,3 @@
import PasswordResetView from './PasswordReset';
export default PasswordResetView;

View File

@ -1,6 +1,6 @@
import React from 'react';
import { FontVariation } from '@harnessio/design-system';
import { Layout, Container, FormInput, ButtonVariation, Text, Button } from '@harnessio/uicore';
import { Layout, Container, ButtonVariation, Text, Button } from '@harnessio/uicore';
import type { UseMutateFunction } from '@tanstack/react-query';
import { Formik, Form } from 'formik';
import { Icon } from '@harnessio/icons';
@ -8,6 +8,7 @@ import * as Yup from 'yup';
import type { ResetPasswordOkResponse, ResetPasswordMutationProps } from '@api/auth';
import { useStrings } from '@strings';
import { PASSWORD_REGEX } from '@constants/validation';
import PasswordInput from '@components/PasswordInput';
interface ResetPasswordViewProps {
handleClose: () => void;
@ -91,22 +92,20 @@ export default function ResetPasswordView(props: ResetPasswordViewProps): React.
return (
<Form style={{ height: '100%' }}>
<Layout.Vertical style={{ gap: '2rem' }}>
<Container>
<FormInput.Text
<Layout.Vertical width="100%" style={{ gap: '0.5rem' }}>
<PasswordInput
name="password"
placeholder={getString('newPassword')}
inputGroup={{ type: 'password' }}
label={<Text font={{ variation: FontVariation.FORM_LABEL }}>{getString('newPassword')}</Text>}
/>
<FormInput.Text
<PasswordInput
name="reEnterPassword"
placeholder={getString('reEnterNewPassword')}
inputGroup={{ type: 'password' }}
label={
<Text font={{ variation: FontVariation.FORM_LABEL }}>{getString('reEnterNewPassword')}</Text>
}
/>
</Container>
</Layout.Vertical>
<Layout.Horizontal style={{ gap: '1rem' }}>
<Button
type="submit"

View File

@ -412,19 +412,21 @@
"operationId": "updatePassword",
"responses": {
"200": {
"description": "OK",
"examples": {
"application/json": {
"message": "password has been reset"
"description": "OK",
"schema": {
"$ref": "#/definitions/response.MessageResponse"
}
},
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
"$ref": "#/definitions/response.ErrOldPassword"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.ErrInvalidCredentials"
}
}
},
@ -1832,6 +1834,40 @@
}
}
]
},
"response.MessageResponse": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
},
"response.ErrOldPassword": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "The old and new passwords can't be same"
},
"errorDescription": {
"type": "string",
"example": "The old and new passwords can't be same"
}
}
},
"response.ErrInvalidCredentials": {
"type": "object",
"properties": {
"error": {
"type": "string",
"example": "The old and new passwords can't be same"
},
"errorDescription": {
"type": "string",
"example": "The old and new passwords can't be same"
}
}
}
}
}