From 5579c017c210a6f26ffa9ea6a608a740c0e9f0f0 Mon Sep 17 00:00:00 2001 From: Hrishav Date: Tue, 25 Jun 2024 20:26:41 +0530 Subject: [PATCH 1/2] feat: Added reset password for new user first login Signed-off-by: Hrishav --- .../authentication/api/docs/swagger.json | 3 + .../authentication/pkg/user/repository.go | 14 +- chaoscenter/web/config/oats.config.ts | 2 +- chaoscenter/web/src/api/auth/index.ts | 1 - chaoscenter/web/src/api/auth/schemas/User.ts | 1 + .../AccountPasswordChange.tsx | 19 +- .../web/src/controllers/Login/LoginPage.tsx | 29 +- .../web/src/controllers/Overview/Overview.tsx | 20 +- chaoscenter/web/src/hooks/useLogout.ts | 1 + .../web/src/routes/RouteDestinations.tsx | 8 +- chaoscenter/web/src/utils/userDetails.ts | 10 +- .../AccountPasswordChange.tsx | 13 +- .../src/views/CreateNewUser/CreateNewUser.tsx | 14 +- .../web/src/views/Overview/Overview.tsx | 46 +- .../Overview/__tests__/Overview.test.tsx | 9 +- mkdocs/docs/auth/v3.0.0/auth-api.json | 4 +- mkdocs/docs/auth/v3.9.0/auth-api.json | 1837 +++++++++++++++++ 17 files changed, 1980 insertions(+), 51 deletions(-) create mode 100644 mkdocs/docs/auth/v3.9.0/auth-api.json diff --git a/chaoscenter/authentication/api/docs/swagger.json b/chaoscenter/authentication/api/docs/swagger.json index a18297f0916..b7f1967169d 100644 --- a/chaoscenter/authentication/api/docs/swagger.json +++ b/chaoscenter/authentication/api/docs/swagger.json @@ -1305,6 +1305,9 @@ }, "username": { "type": "string" + }, + "isInitialLogin": { + "type": "boolean" } } } diff --git a/chaoscenter/authentication/pkg/user/repository.go b/chaoscenter/authentication/pkg/user/repository.go index 30090d9b28d..e32495459c6 100644 --- a/chaoscenter/authentication/pkg/user/repository.go +++ b/chaoscenter/authentication/pkg/user/repository.go @@ -2,6 +2,7 @@ package user import ( "context" + "errors" "github.com/litmuschaos/litmus/chaoscenter/authentication/pkg/entities" "github.com/litmuschaos/litmus/chaoscenter/authentication/pkg/utils" @@ -169,6 +170,13 @@ func (r repository) CheckPasswordHash(hash, password string) error { // UpdatePassword helps to update the password of the user, it acts as a resetPassword when isAdminBeingReset is set to false func (r repository) UpdatePassword(userPassword *entities.UserPassword, isAdminBeingReset bool) error { var result = entities.User{} + result.Username = userPassword.Username + findOneErr := r.Collection.FindOne(context.TODO(), bson.M{ + "username": result.Username, + }).Decode(&result) + if findOneErr != nil { + return findOneErr + } newHashedPassword, err := bcrypt.GenerateFromPassword([]byte(userPassword.NewPassword), utils.PasswordEncryptionCost) updateQuery := bson.M{"$set": bson.M{ @@ -188,11 +196,13 @@ func (r repository) UpdatePassword(userPassword *entities.UserPassword, isAdminB }} } - _, err = r.Collection.UpdateOne(context.Background(), bson.M{"username": result.ID}, updateQuery) + res, err := r.Collection.UpdateOne(context.Background(), bson.M{"username": result.Username}, updateQuery) if err != nil { return err } - + if res.MatchedCount == 0 { + return errors.New("could not find matching username in database") + } return nil } diff --git a/chaoscenter/web/config/oats.config.ts b/chaoscenter/web/config/oats.config.ts index 5c59fca3574..68d2f375765 100644 --- a/chaoscenter/web/config/oats.config.ts +++ b/chaoscenter/web/config/oats.config.ts @@ -9,7 +9,7 @@ function normalizePath(url: string): string { export default defineConfig({ services: { auth: { - file: '../../mkdocs/docs/auth/v3.0.0/auth-api.json', + file: '../../mkdocs/docs/auth/v3.9.0/auth-api.json', output: 'src/api/auth', transformer(spec) { return { diff --git a/chaoscenter/web/src/api/auth/index.ts b/chaoscenter/web/src/api/auth/index.ts index a221645dfd0..4d040732a26 100644 --- a/chaoscenter/web/src/api/auth/index.ts +++ b/chaoscenter/web/src/api/auth/index.ts @@ -206,7 +206,6 @@ export type { UpdateStateProps, UpdateStateRequestBody } from './hooks/useUpdateStateMutation'; - export { updateState, useUpdateStateMutation } from './hooks/useUpdateStateMutation'; export type { UsersErrorResponse, UsersOkResponse, UsersProps } from './hooks/useUsersQuery'; export { useUsersQuery, users } from './hooks/useUsersQuery'; diff --git a/chaoscenter/web/src/api/auth/schemas/User.ts b/chaoscenter/web/src/api/auth/schemas/User.ts index 30d22696c7d..81a5f500bf8 100644 --- a/chaoscenter/web/src/api/auth/schemas/User.ts +++ b/chaoscenter/web/src/api/auth/schemas/User.ts @@ -7,6 +7,7 @@ export interface User { createdAt?: number; createdBy?: ActionBy; email?: string; + isInitialLogin?: boolean; isRemoved: boolean; name?: string; role: string; diff --git a/chaoscenter/web/src/controllers/AccountPasswordChange/AccountPasswordChange.tsx b/chaoscenter/web/src/controllers/AccountPasswordChange/AccountPasswordChange.tsx index 8d7dad92743..7152bc73db1 100644 --- a/chaoscenter/web/src/controllers/AccountPasswordChange/AccountPasswordChange.tsx +++ b/chaoscenter/web/src/controllers/AccountPasswordChange/AccountPasswordChange.tsx @@ -1,27 +1,37 @@ 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 } from '@hooks'; +import { useLogout, useRouteWithBaseUrl } 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 { showSuccess } = useToaster(); const { getString } = useStrings(); + const history = useHistory(); + const paths = useRouteWithBaseUrl(); const { forceLogout } = useLogout(); - const { handleClose, username } = props; const { mutate: updatePasswordMutation, isLoading } = useUpdatePasswordMutation( {}, { onSuccess: data => { - showSuccess(`${data.message}, ${getString('loginToContinue')}`); - forceLogout(); + setUserDetails({ isInitialLogin: false }); + if (initialMode) { + history.push(paths.toDashboard()); + } else { + showSuccess(`${data.message}, ${getString('loginToContinue')}`); + forceLogout(); + } } } ); @@ -32,6 +42,7 @@ 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 0798d7fdb5a..2345b12210f 100644 --- a/chaoscenter/web/src/controllers/Login/LoginPage.tsx +++ b/chaoscenter/web/src/controllers/Login/LoginPage.tsx @@ -3,8 +3,8 @@ import { useHistory } from 'react-router-dom'; import { useToaster } from '@harnessio/uicore'; import jwtDecode from 'jwt-decode'; import LoginPageView from '@views/Login'; -import { useLoginMutation, useGetCapabilitiesQuery } from '@api/auth'; -import { setUserDetails } from '@utils'; +import { useLoginMutation, useGetCapabilitiesQuery, useGetUserQuery } from '@api/auth'; +import { getUserDetails, setUserDetails } from '@utils'; import { normalizePath } from '@routes/RouteDefinitions'; import type { DecodedTokenType, PermissionGroup } from '@models'; import { useSearchParams } from '@hooks'; @@ -13,10 +13,13 @@ const LoginController: React.FC = () => { const history = useHistory(); const { showError } = useToaster(); const searchParams = useSearchParams(); + const dexToken = searchParams.get('jwtToken'); const dexProjectID = searchParams.get('projectID'); const dexProjectRole = searchParams.get('projectRole') as PermissionGroup; + const [activateGetAPI, setActivateGetAPI] = React.useState(false); + const capabilities = useGetCapabilitiesQuery({}); React.useEffect(() => { @@ -37,15 +40,33 @@ const LoginController: React.FC = () => { onError: err => showError(err.error), onSuccess: response => { if (response.accessToken) { - const accountID = (jwtDecode(response.accessToken) as DecodedTokenType).uid; setUserDetails(response); - history.push(normalizePath(`/account/${accountID}/project/${response.projectID ?? ''}/dashboard`)); + setActivateGetAPI(true); } }, retry: false } ); + const userDetails = getUserDetails(); + + useGetUserQuery( + { + user_id: userDetails.accountID + }, + { + onSuccess: response => { + setUserDetails({ + isInitialLogin: response.isInitialLogin + }); + history.push( + normalizePath(`/account/${userDetails.accountID}/project/${userDetails.projectID ?? ''}/dashboard`) + ); + }, + enabled: activateGetAPI + } + ); + return ; }; diff --git a/chaoscenter/web/src/controllers/Overview/Overview.tsx b/chaoscenter/web/src/controllers/Overview/Overview.tsx index 56c569734b0..87e557a0fe9 100644 --- a/chaoscenter/web/src/controllers/Overview/Overview.tsx +++ b/chaoscenter/web/src/controllers/Overview/Overview.tsx @@ -1,14 +1,16 @@ import React from 'react'; import { useToaster } from '@harnessio/uicore'; import { getChaosHubStats, getExperimentStats, getInfraStats, listExperiment } from '@api/core'; -import { getScope } from '@utils'; +import { getScope, getUserDetails } 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 @@ -27,9 +29,6 @@ export default function OverviewController(): React.ReactElement { refetch: refetchExperiments } = listExperiment({ ...scope, - // filter: { - // infraTypes: [InfrastructureType.KUBERNETES] - // }, pagination: { page: 0, limit: 7 }, options: { onError: error => showError(error.message), @@ -38,6 +37,15 @@ 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 && { @@ -50,8 +58,10 @@ export default function OverviewController(): React.ReactElement { chaosHubStats: loadingChaosHubStats, infraStats: loadingInfraStats, experimentStats: loadingExperimentStats, - recentExperimentsTable: loadingRecentExperimentsTable + recentExperimentsTable: loadingRecentExperimentsTable, + getUser: getUserLoading }} + currentUserData={currentUserData} chaosHubStats={chaosHubStats?.getChaosHubStats} infraStats={infraStats?.getInfraStats} experimentStats={experimentStats?.getExperimentStats} diff --git a/chaoscenter/web/src/hooks/useLogout.ts b/chaoscenter/web/src/hooks/useLogout.ts index a38103b0845..7bb9c8ea672 100644 --- a/chaoscenter/web/src/hooks/useLogout.ts +++ b/chaoscenter/web/src/hooks/useLogout.ts @@ -15,6 +15,7 @@ export const useLogout = (): UseLogoutReturn => { localStorage.removeItem('accessToken'); localStorage.removeItem('projectRole'); localStorage.removeItem('projectID'); + localStorage.removeItem('isInitialLogin'); history.push(paths.toLogin()); }, retry: false diff --git a/chaoscenter/web/src/routes/RouteDestinations.tsx b/chaoscenter/web/src/routes/RouteDestinations.tsx index cae43934fe0..992095c37fb 100644 --- a/chaoscenter/web/src/routes/RouteDestinations.tsx +++ b/chaoscenter/web/src/routes/RouteDestinations.tsx @@ -42,16 +42,20 @@ export function RoutesWithAuthentication(): React.ReactElement { const projectRenderPaths = useRouteWithBaseUrl(); const accountMatchPaths = useRouteDefinitionsMatch('account'); const accountRenderPaths = useRouteDefinitionsMatch('account'); + const history = useHistory(); const { forceLogout } = useLogout(); - const { accessToken: token } = getUserDetails(); + const { accessToken: token, isInitialLogin } = getUserDetails(); useEffect(() => { if (!token || !isUserAuthenticated()) { forceLogout(); } + if (isInitialLogin) { + history.push(projectRenderPaths.toDashboard()); + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [token]); + }, [token, isInitialLogin]); return ( diff --git a/chaoscenter/web/src/utils/userDetails.ts b/chaoscenter/web/src/utils/userDetails.ts index c676a7d4c2f..094850485e4 100644 --- a/chaoscenter/web/src/utils/userDetails.ts +++ b/chaoscenter/web/src/utils/userDetails.ts @@ -1,12 +1,13 @@ import jwtDecode from 'jwt-decode'; import type { DecodedTokenType } from '@models'; -interface UserDetailsProps { +export interface UserDetailsProps { accessToken: string; projectRole: string; projectID: string; accountID: string; accountRole: string; + isInitialLogin: boolean; } export function decode(arg: string): T { @@ -19,15 +20,18 @@ export function getUserDetails(): UserDetailsProps { const accountRole = accessToken ? (jwtDecode(accessToken) as DecodedTokenType).role : ''; const projectRole = localStorage.getItem('projectRole') ?? ''; const projectID = localStorage.getItem('projectID') ?? ''; - return { accessToken, projectRole, projectID, accountID, accountRole }; + const isInitialLogin = localStorage.getItem('isInitialLogin') === 'true'; + return { accessToken, projectRole, projectID, accountID, accountRole, isInitialLogin }; } export function setUserDetails({ accessToken, projectRole, - projectID + projectID, + isInitialLogin }: Partial>): void { if (accessToken) localStorage.setItem('accessToken', accessToken); if (projectRole) localStorage.setItem('projectRole', projectRole); if (projectID) localStorage.setItem('projectID', projectID); + if (isInitialLogin !== undefined) localStorage.setItem('isInitialLogin', `${isInitialLogin}`); } diff --git a/chaoscenter/web/src/views/AccountPasswordChange/AccountPasswordChange.tsx b/chaoscenter/web/src/views/AccountPasswordChange/AccountPasswordChange.tsx index 36867145112..6ec5a52c104 100644 --- a/chaoscenter/web/src/views/AccountPasswordChange/AccountPasswordChange.tsx +++ b/chaoscenter/web/src/views/AccountPasswordChange/AccountPasswordChange.tsx @@ -19,6 +19,7 @@ interface AccountPasswordChangeViewProps { unknown >; updatePasswordMutationLoading: boolean; + initialMode?: boolean; } interface AccountPasswordChangeFormProps { oldPassword: string; @@ -27,7 +28,7 @@ interface AccountPasswordChangeFormProps { } export default function AccountPasswordChangeView(props: AccountPasswordChangeViewProps): React.ReactElement { - const { handleClose, updatePasswordMutation, updatePasswordMutationLoading, username } = props; + const { handleClose, updatePasswordMutation, updatePasswordMutationLoading, username, initialMode } = props; const { getString } = useStrings(); const { showError } = useToaster(); @@ -68,7 +69,7 @@ export default function AccountPasswordChangeView(props: AccountPasswordChangeVi {getString('updatePassword')} - handleClose()} /> + {!initialMode && } @@ -125,11 +126,9 @@ export default function AccountPasswordChangeView(props: AccountPasswordChangeVi disabled={updatePasswordMutationLoading || isSubmitButtonDisabled(formikProps.values)} style={{ minWidth: '90px' }} /> -