Skip to content

Commit

Permalink
feat: Added reset password as a mandatory step if new user logs in (l…
Browse files Browse the repository at this point in the history
…itmuschaos#4729)

* feat: Added reset password for new user first login

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

* feat: Resolved few Codacy failing checks

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

---------

Signed-off-by: Hrishav <hrishav.kumar@harness.io>
  • Loading branch information
hrishavjha authored Jun 26, 2024
1 parent 5e52f7f commit 16ea8cb
Show file tree
Hide file tree
Showing 17 changed files with 1,978 additions and 50 deletions.
3 changes: 3 additions & 0 deletions chaoscenter/authentication/api/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,9 @@
},
"username": {
"type": "string"
},
"isInitialLogin": {
"type": "boolean"
}
}
}
Expand Down
14 changes: 12 additions & 2 deletions chaoscenter/authentication/pkg/user/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand All @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion chaoscenter/web/config/oats.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion chaoscenter/web/src/api/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions chaoscenter/web/src/api/auth/schemas/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface User {
createdAt?: number;
createdBy?: ActionBy;
email?: string;
isInitialLogin?: boolean;
isRemoved: boolean;
name?: string;
role: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
);
Expand All @@ -32,6 +42,7 @@ export default function AccountPasswordChangeController(props: AccountPasswordCh
updatePasswordMutation={updatePasswordMutation}
updatePasswordMutationLoading={isLoading}
username={username}
initialMode={initialMode}
/>
);
}
29 changes: 25 additions & 4 deletions chaoscenter/web/src/controllers/Login/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<boolean>(false);

const capabilities = useGetCapabilitiesQuery({});

React.useEffect(() => {
Expand All @@ -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
},
{
enabled: activateGetAPI,
onSuccess: response => {
setUserDetails({
isInitialLogin: response.isInitialLogin
});
history.push(
normalizePath(`/account/${userDetails.accountID}/project/${userDetails.projectID ?? ''}/dashboard`)
);
}
}
);

return <LoginPageView handleLogin={handleLogin} loading={isLoading} capabilities={capabilities.data} />;
};

Expand Down
20 changes: 15 additions & 5 deletions chaoscenter/web/src/controllers/Overview/Overview.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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),
Expand All @@ -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 && {
Expand All @@ -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}
Expand Down
1 change: 1 addition & 0 deletions chaoscenter/web/src/hooks/useLogout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions chaoscenter/web/src/routes/RouteDestinations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Switch>
Expand Down
10 changes: 7 additions & 3 deletions chaoscenter/web/src/utils/userDetails.ts
Original file line number Diff line number Diff line change
@@ -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<T = unknown>(arg: string): T {
Expand All @@ -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<Omit<UserDetailsProps, 'accountID' | 'accountRole'>>): 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}`);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface AccountPasswordChangeViewProps {
unknown
>;
updatePasswordMutationLoading: boolean;
initialMode?: boolean;
}
interface AccountPasswordChangeFormProps {
oldPassword: string;
Expand All @@ -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();

Expand Down Expand Up @@ -68,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>
<Icon name="cross" style={{ cursor: 'pointer' }} size={18} onClick={() => handleClose()} />
{!initialMode && <Icon name="cross" style={{ cursor: 'pointer' }} size={18} onClick={handleClose} />}
</Layout.Horizontal>
<Container>
<Formik<AccountPasswordChangeFormProps>
Expand Down Expand Up @@ -125,11 +126,9 @@ export default function AccountPasswordChangeView(props: AccountPasswordChangeVi
disabled={updatePasswordMutationLoading || isSubmitButtonDisabled(formikProps.values)}
style={{ minWidth: '90px' }}
/>
<Button
variation={ButtonVariation.TERTIARY}
text={getString('cancel')}
onClick={() => handleClose()}
/>
{!initialMode && (
<Button variation={ButtonVariation.TERTIARY} text={getString('cancel')} onClick={handleClose} />
)}
</Layout.Horizontal>
</Layout.Vertical>
</Form>
Expand Down
14 changes: 3 additions & 11 deletions chaoscenter/web/src/views/CreateNewUser/CreateNewUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Icon } from '@harnessio/icons';
import * as Yup from 'yup';
import type { CreateUserMutationProps, User } from '@api/auth';
import { useStrings } from '@strings';
import { PASSWORD_REGEX, USERNAME_REGEX } from '@constants/validation';
import { USERNAME_REGEX } from '@constants/validation';

interface CreateNewUserViewProps {
createNewUserMutation: UseMutateFunction<User, unknown, CreateUserMutationProps<never>, unknown>;
Expand Down Expand Up @@ -70,11 +70,7 @@ export default function CreateNewUserView(props: CreateNewUserViewProps): React.
.min(3, getString('fieldMinLength', { length: 3 }))
.max(16, getString('fieldMaxLength', { length: 16 }))
.matches(USERNAME_REGEX, getString('usernameValidText')),
password: Yup.string()
.required(getString('passwordIsRequired'))
.min(8, getString('fieldMinLength', { length: 8 }))
.max(16, getString('fieldMaxLength', { length: 16 }))
.matches(PASSWORD_REGEX, getString('passwordValidation')),
password: Yup.string().required(getString('passwordIsRequired')),
reEnterPassword: Yup.string()
.required(getString('reEnterPassword'))
.oneOf([Yup.ref('password'), null], getString('passwordsDoNotMatch'))
Expand Down Expand Up @@ -125,11 +121,7 @@ export default function CreateNewUserView(props: CreateNewUserViewProps): React.
disabled={createNewUserMutationLoading || Object.keys(formikProps.errors).length > 0}
style={{ minWidth: '90px' }}
/>
<Button
variation={ButtonVariation.TERTIARY}
text={getString('cancel')}
onClick={() => handleClose()}
/>
<Button variation={ButtonVariation.TERTIARY} text={getString('cancel')} onClick={handleClose} />
</Layout.Horizontal>
</Layout.Vertical>
</Form>
Expand Down
Loading

0 comments on commit 16ea8cb

Please sign in to comment.