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:
parent
00f0bd7366
commit
fb46bb9334
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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`)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,3 @@
|
|||
import PasswordResetController from './PasswordReset';
|
||||
|
||||
export default PasswordResetController;
|
|
@ -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',
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.formContainer {
|
||||
border-radius: 4px;
|
||||
transform: translateY(-82px);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
declare namespace PasswordResetModuleScssNamespace {
|
||||
export interface IPasswordResetModuleScss {
|
||||
formContainer: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare const PasswordResetModuleScssModule: PasswordResetModuleScssNamespace.IPasswordResetModuleScss;
|
||||
|
||||
export = PasswordResetModuleScssModule;
|
|
@ -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;
|
|
@ -0,0 +1,3 @@
|
|||
import PasswordResetView from './PasswordReset';
|
||||
|
||||
export default PasswordResetView;
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue