From fb46bb9334cd3de37deb0b9d03e021e88fed557d Mon Sep 17 00:00:00 2001 From: Hrishav Date: Fri, 5 Jul 2024 12:37:36 +0530 Subject: [PATCH] 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 * feat: Fixed deepspan issue Signed-off-by: Hrishav * fix: Updated API response in front-end Signed-off-by: Hrishav * chore: addressed review comment Signed-off-by: Hrishav --------- Signed-off-by: Hrishav --- .../api/handlers/rest/user_handlers.go | 4 +- .../authentication/pkg/user/repository.go | 2 +- .../auth/hooks/useUpdatePasswordMutation.ts | 9 +- chaoscenter/web/src/api/auth/index.ts | 3 + .../schemas/ResponseErrInvalidCredentials.ts | 14 ++ .../auth/schemas/ResponseErrOldPassword.ts | 14 ++ .../auth/schemas/ResponseMessageResponse.ts | 7 + .../PasswordInput/PasswordInput.tsx | 6 +- .../AccountPasswordChange.tsx | 17 +-- .../web/src/controllers/Login/LoginPage.tsx | 20 ++- .../web/src/controllers/Overview/Overview.tsx | 17 +-- .../PasswordReset/PasswordReset.tsx | 47 +++++++ .../src/controllers/PasswordReset/index.ts | 3 + .../web/src/routes/RouteDefinitions.ts | 2 + .../web/src/routes/RouteDestinations.tsx | 10 +- .../AccountPasswordChange.tsx | 35 ++--- .../web/src/views/Overview/Overview.tsx | 36 +---- .../PasswordReset/PasswordReset.module.scss | 4 + .../PasswordReset.module.scss.d.ts | 9 ++ .../src/views/PasswordReset/PasswordReset.tsx | 128 ++++++++++++++++++ .../web/src/views/PasswordReset/index.ts | 3 + .../src/views/ResetPassword/ResetPassword.tsx | 13 +- mkdocs/docs/auth/v3.9.0/auth-api.json | 58 ++++++-- 23 files changed, 347 insertions(+), 114 deletions(-) create mode 100644 chaoscenter/web/src/api/auth/schemas/ResponseErrInvalidCredentials.ts create mode 100644 chaoscenter/web/src/api/auth/schemas/ResponseErrOldPassword.ts create mode 100644 chaoscenter/web/src/api/auth/schemas/ResponseMessageResponse.ts create mode 100644 chaoscenter/web/src/controllers/PasswordReset/PasswordReset.tsx create mode 100644 chaoscenter/web/src/controllers/PasswordReset/index.ts create mode 100644 chaoscenter/web/src/views/PasswordReset/PasswordReset.module.scss create mode 100644 chaoscenter/web/src/views/PasswordReset/PasswordReset.module.scss.d.ts create mode 100644 chaoscenter/web/src/views/PasswordReset/PasswordReset.tsx create mode 100644 chaoscenter/web/src/views/PasswordReset/index.ts diff --git a/chaoscenter/authentication/api/handlers/rest/user_handlers.go b/chaoscenter/authentication/api/handlers/rest/user_handlers.go index afd4a04fbbe..6b7336cfd19 100644 --- a/chaoscenter/authentication/api/handlers/rest/user_handlers.go +++ b/chaoscenter/authentication/api/handlers/rest/user_handlers.go @@ -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 } diff --git a/chaoscenter/authentication/pkg/user/repository.go b/chaoscenter/authentication/pkg/user/repository.go index 09dd127dd5f..ca0352d3d98 100644 --- a/chaoscenter/authentication/pkg/user/repository.go +++ b/chaoscenter/authentication/pkg/user/repository.go @@ -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)) diff --git a/chaoscenter/web/src/api/auth/hooks/useUpdatePasswordMutation.ts b/chaoscenter/web/src/api/auth/hooks/useUpdatePasswordMutation.ts index 78581a4a069..394b271908c 100644 --- a/chaoscenter/web/src/api/auth/hooks/useUpdatePasswordMutation.ts +++ b/chaoscenter/web/src/api/auth/hooks/useUpdatePasswordMutation.ts @@ -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, 'url'> { body: UpdatePasswordRequestBody; diff --git a/chaoscenter/web/src/api/auth/index.ts b/chaoscenter/web/src/api/auth/index.ts index 4d040732a26..aeaf856e414 100644 --- a/chaoscenter/web/src/api/auth/index.ts +++ b/chaoscenter/web/src/api/auth/index.ts @@ -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'; diff --git a/chaoscenter/web/src/api/auth/schemas/ResponseErrInvalidCredentials.ts b/chaoscenter/web/src/api/auth/schemas/ResponseErrInvalidCredentials.ts new file mode 100644 index 00000000000..bf89d830624 --- /dev/null +++ b/chaoscenter/web/src/api/auth/schemas/ResponseErrInvalidCredentials.ts @@ -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; +} diff --git a/chaoscenter/web/src/api/auth/schemas/ResponseErrOldPassword.ts b/chaoscenter/web/src/api/auth/schemas/ResponseErrOldPassword.ts new file mode 100644 index 00000000000..e9b3c8d08f7 --- /dev/null +++ b/chaoscenter/web/src/api/auth/schemas/ResponseErrOldPassword.ts @@ -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; +} diff --git a/chaoscenter/web/src/api/auth/schemas/ResponseMessageResponse.ts b/chaoscenter/web/src/api/auth/schemas/ResponseMessageResponse.ts new file mode 100644 index 00000000000..c5d0ba97b2c --- /dev/null +++ b/chaoscenter/web/src/api/auth/schemas/ResponseMessageResponse.ts @@ -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; +} diff --git a/chaoscenter/web/src/components/PasswordInput/PasswordInput.tsx b/chaoscenter/web/src/components/PasswordInput/PasswordInput.tsx index e889b686420..8d26514b59d 100644 --- a/chaoscenter/web/src/components/PasswordInput/PasswordInput.tsx +++ b/chaoscenter/web/src/components/PasswordInput/PasswordInput.tsx @@ -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 ( - {label && ( + {label && typeof label === 'string' ? ( {label} + ) : ( + label )}
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} /> ); } diff --git a/chaoscenter/web/src/controllers/Login/LoginPage.tsx b/chaoscenter/web/src/controllers/Login/LoginPage.tsx index 6578b31edba..ee2f462fcec 100644 --- a/chaoscenter/web/src/controllers/Login/LoginPage.tsx +++ b/chaoscenter/web/src/controllers/Login/LoginPage.tsx @@ -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`) + ); + } } } ); diff --git a/chaoscenter/web/src/controllers/Overview/Overview.tsx b/chaoscenter/web/src/controllers/Overview/Overview.tsx index 87e557a0fe9..da00f4e4acd 100644 --- a/chaoscenter/web/src/controllers/Overview/Overview.tsx +++ b/chaoscenter/web/src/controllers/Overview/Overview.tsx @@ -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} diff --git a/chaoscenter/web/src/controllers/PasswordReset/PasswordReset.tsx b/chaoscenter/web/src/controllers/PasswordReset/PasswordReset.tsx new file mode 100644 index 00000000000..db808765d07 --- /dev/null +++ b/chaoscenter/web/src/controllers/PasswordReset/PasswordReset.tsx @@ -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 ( + + ); +}; + +export default PasswordResetController; diff --git a/chaoscenter/web/src/controllers/PasswordReset/index.ts b/chaoscenter/web/src/controllers/PasswordReset/index.ts new file mode 100644 index 00000000000..b2867e5f041 --- /dev/null +++ b/chaoscenter/web/src/controllers/PasswordReset/index.ts @@ -0,0 +1,3 @@ +import PasswordResetController from './PasswordReset'; + +export default PasswordResetController; diff --git a/chaoscenter/web/src/routes/RouteDefinitions.ts b/chaoscenter/web/src/routes/RouteDefinitions.ts index 678b51b3fcb..d030df79934 100644 --- a/chaoscenter/web/src/routes/RouteDefinitions.ts +++ b/chaoscenter/web/src/routes/RouteDefinitions.ts @@ -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', diff --git a/chaoscenter/web/src/routes/RouteDestinations.tsx b/chaoscenter/web/src/routes/RouteDestinations.tsx index 992095c37fb..a5d31a7262b 100644 --- a/chaoscenter/web/src/routes/RouteDestinations.tsx +++ b/chaoscenter/web/src/routes/RouteDestinations.tsx @@ -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 { - + {/* Account */} + + + {/* Dashboard */} {/* Chaos Experiments */} diff --git a/chaoscenter/web/src/views/AccountPasswordChange/AccountPasswordChange.tsx b/chaoscenter/web/src/views/AccountPasswordChange/AccountPasswordChange.tsx index 6ec5a52c104..0b8e24a15c5 100644 --- a/chaoscenter/web/src/views/AccountPasswordChange/AccountPasswordChange.tsx +++ b/chaoscenter/web/src/views/AccountPasswordChange/AccountPasswordChange.tsx @@ -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, 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 {getString('updatePassword')} - {!initialMode && } + @@ -95,29 +95,26 @@ export default function AccountPasswordChangeView(props: AccountPasswordChangeVi return (
- - + {getString('oldPassword')}} /> - {getString('newPassword')}} /> - {getString('reEnterNewPassword')} } /> - - + +