Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added reset password as a mandatory step if new user logs in #4729

Merged
merged 2 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@

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 @@
// 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)
Comment on lines +174 to +176

Check failure

Code scanning / CodeQL

Database query built from user-controlled sources High

This query depends on a
user-provided value
.
This query depends on a
user-provided value
.
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 @@
}}
}

_, 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)

Check failure

Code scanning / CodeQL

Database query built from user-controlled sources High

This query depends on a
user-provided value
.
This query depends on a
user-provided value
.
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
Loading