Skip to content

Commit

Permalink
feat: Updated login and update password APIs (#4751)
Browse files Browse the repository at this point in the history
* fix: Added error response in createUser API

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

* fix: added project ID to update password mutation

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

* fix: fixed failing UTs

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

---------

Signed-off-by: Hrishav <hrishav.kumar@harness.io>
hrishavjha authored Jul 9, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent a02767d commit 7503f0d
Showing 14 changed files with 160 additions and 34 deletions.
59 changes: 58 additions & 1 deletion chaoscenter/authentication/api/handlers/rest/user_handlers.go
Original file line number Diff line number Diff line change
@@ -430,6 +430,15 @@ func UpdatePassword(service services.ApplicationService) gin.HandlerFunc {
return
}
username := c.MustGet("username").(string)

// Fetching userDetails
user, err := service.FindUserByUsername(username)
if err != nil {
log.Error(err)
c.JSON(utils.ErrorStatusCodes[utils.ErrUserNotFound], presenter.CreateErrorResponse(utils.ErrInvalidCredentials))
return
}

userPasswordRequest.Username = username
if userPasswordRequest.NewPassword != "" {
err := utils.ValidateStrictPassword(userPasswordRequest.NewPassword)
@@ -454,8 +463,56 @@ func UpdatePassword(service services.ApplicationService) gin.HandlerFunc {
}
return
}

var defaultProject string
ownerProjects, err := service.GetOwnerProjectIDs(c, user.ID)

if len(ownerProjects) > 0 {
defaultProject = ownerProjects[0].ID
} else {
// Adding user as project owner in project's member list
newMember := &entities.Member{
UserID: user.ID,
Role: entities.RoleOwner,
Invitation: entities.AcceptedInvitation,
Username: user.Username,
Name: user.Name,
Email: user.Email,
JoinedAt: time.Now().UnixMilli(),
}
var members []*entities.Member
members = append(members, newMember)
state := "active"
newProject := &entities.Project{
ID: uuid.Must(uuid.NewRandom()).String(),
Name: user.Username + "-project",
Members: members,
State: &state,
Audit: entities.Audit{
IsRemoved: false,
CreatedAt: time.Now().UnixMilli(),
CreatedBy: entities.UserDetailResponse{
Username: user.Username,
UserID: user.ID,
Email: user.Email,
},
UpdatedAt: time.Now().UnixMilli(),
UpdatedBy: entities.UserDetailResponse{
Username: user.Username,
UserID: user.ID,
Email: user.Email,
},
},
}
err := service.CreateProject(newProject)
if err != nil {
return
}
defaultProject = newProject.ID
}
c.JSON(http.StatusOK, gin.H{
"message": "password has been updated successfully",
"message": "password has been updated successfully",
"projectID": defaultProject,
})
}
}
14 changes: 12 additions & 2 deletions chaoscenter/authentication/api/handlers/rest/user_handlers_test.go
Original file line number Diff line number Diff line change
@@ -490,7 +490,7 @@ func TestUpdatePassword(t *testing.T) {
givenStrictPassword: false,
givenServiceResponse: nil,
expectedCode: http.StatusOK,
expectedOutput: `{"message":"password has been updated successfully"}`,
expectedOutput: `{"message":"password has been updated successfully","projectID":"someProjectID"}`,
},
{
name: "Invalid new password",
@@ -524,9 +524,19 @@ func TestUpdatePassword(t *testing.T) {
Email: "test@example.com",
IsInitialLogin: false,
}
userFromDB := &entities.User{
ID: "testUserID",
Username: "testUser",
Password: "hashedPassword",
Email: "test@example.com",
}
service.On("FindUserByUsername", "testUser").Return(userFromDB, nil)
service.On("GetUser", "testUID").Return(user, nil)
service.On("UpdatePassword", &userPassword, true).Return(tt.givenServiceResponse)

project := &entities.Project{
ID: "someProjectID",
}
service.On("GetOwnerProjectIDs", mock.Anything, "testUserID").Return([]*entities.Project{project}, nil)
rest.UpdatePassword(service)(c)

assert.Equal(t, tt.expectedCode, w.Code)
9 changes: 5 additions & 4 deletions chaoscenter/authentication/api/main.go
Original file line number Diff line number Diff line change
@@ -157,10 +157,11 @@ func validatedAdminSetup(service services.ApplicationService) {
password := string(hashedPassword)

adminUser := entities.User{
ID: uID,
Username: utils.AdminName,
Password: password,
Role: entities.RoleAdmin,
ID: uID,
Username: utils.AdminName,
Password: password,
Role: entities.RoleAdmin,
IsInitialLogin: true,
Audit: entities.Audit{
CreatedAt: time.Now().UnixMilli(),
UpdatedAt: time.Now().UnixMilli(),
3 changes: 2 additions & 1 deletion chaoscenter/web/src/api/auth/hooks/useCreateUserMutation.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query';

import type { User } from '../schemas/User';
import type { ResponseErrBadRequest } from '../schemas/ResponseErrBadRequest';
import { fetcher, FetcherOptions } from 'services/fetcher';

export type CreateUserRequestBody = {
@@ -17,7 +18,7 @@ export type CreateUserRequestBody = {

export type CreateUserOkResponse = User;

export type CreateUserErrorResponse = unknown;
export type CreateUserErrorResponse = ResponseErrBadRequest;

export interface CreateUserProps extends Omit<FetcherOptions<unknown, CreateUserRequestBody>, 'url'> {
body: CreateUserRequestBody;
1 change: 1 addition & 0 deletions chaoscenter/web/src/api/auth/index.ts
Original file line number Diff line number Diff line change
@@ -223,6 +223,7 @@ 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 { ResponseErrBadRequest } from './schemas/ResponseErrBadRequest';
export type { ResponseErrInvalidCredentials } from './schemas/ResponseErrInvalidCredentials';
export type { ResponseErrOldPassword } from './schemas/ResponseErrOldPassword';
export type { ResponseMessageResponse } from './schemas/ResponseMessageResponse';
14 changes: 14 additions & 0 deletions chaoscenter/web/src/api/auth/schemas/ResponseErrBadRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-disable */
// This code is autogenerated using @harnessio/oats-cli.
// Please do not modify this code directly.

export interface ResponseErrBadRequest {
/**
* @example "The old and new passwords can't be same"
*/
error?: string;
/**
* @example "The old and new passwords can't be same"
*/
errorDescription?: string;
}
Original file line number Diff line number Diff line change
@@ -3,5 +3,6 @@
// Please do not modify this code directly.

export interface ResponseMessageResponse {
message?: string;
message: string;
projectID: string;
}
Original file line number Diff line number Diff line change
@@ -2,17 +2,19 @@ import React from 'react';
import { Icon, IconName } from '@harnessio/icons';
import { FormInput, Layout, Text } from '@harnessio/uicore';
import { Color, FontVariation } from '@harnessio/design-system';
import cx from 'classnames';
import style from './PasswordInput.module.scss';

interface PasswordInputProps {
disabled?: boolean;
placeholder?: string;
name: string;
label: string | React.ReactElement;
className?: string;
}

const PasswordInput = (props: PasswordInputProps): React.ReactElement => {
const { disabled, label, name, placeholder } = props;
const { disabled, label, name, placeholder, className } = props;
const [showPassword, setShowPassword] = React.useState(false);
const stateIcon: IconName = showPassword ? 'eye-off' : 'eye-open';

@@ -21,7 +23,7 @@ const PasswordInput = (props: PasswordInputProps): React.ReactElement => {
}

return (
<Layout.Vertical className={style.fieldContainer}>
<Layout.Vertical className={cx(style.fieldContainer, className)}>
{label && typeof label === 'string' ? (
<Text font={{ variation: FontVariation.BODY, weight: 'semi-bold' }} color={Color.GREY_600}>
{label}
Original file line number Diff line number Diff line change
@@ -14,15 +14,19 @@ interface CreateNewUserControllerProps {

export default function CreateNewUserController(props: CreateNewUserControllerProps): React.ReactElement {
const { getUsersRefetch, handleClose } = props;
const { showSuccess } = useToaster();
const { showSuccess, showError } = useToaster();
const { getString } = useStrings();

const { mutate: createNewUserMutation, isLoading } = useCreateUserMutation(
{},
{
onSuccess: data => {
getUsersRefetch();
handleClose();
showSuccess(getString('userCreateSuccessMessage', { name: data.name }));
},
onError: e => {
showError(e.errorDescription);
}
}
);
Original file line number Diff line number Diff line change
@@ -5,11 +5,13 @@ import PasswordResetView from '@views/PasswordReset';
import { useGetUserQuery, useUpdatePasswordMutation } from '@api/auth';
import { getUserDetails, setUserDetails } from '@utils';
import { normalizePath } from '@routes/RouteDefinitions';
import { useAppStore } from '@context';

const PasswordResetController = (): React.ReactElement => {
const { accountID, projectID } = getUserDetails();
const { accountID } = getUserDetails();
const { showSuccess, showError } = useToaster();
const history = useHistory();
const { updateAppStore } = useAppStore();

const { data: currentUserData, isLoading: getUserLoading } = useGetUserQuery(
{
@@ -26,7 +28,8 @@ const PasswordResetController = (): React.ReactElement => {
onSuccess: data => {
setUserDetails({ isInitialLogin: false });
showSuccess(`${data.message}`);
history.push(normalizePath(`/account/${accountID}/project/${projectID}/dashboard`));
updateAppStore({ projectID: data.projectID });
history.push(normalizePath(`/account/${accountID}/project/${data.projectID}/dashboard`));
},
onError: err => showError(err.errorDescription)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.passwordField {
margin-bottom: 15px !important;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare namespace CreateNewUserModuleScssNamespace {
export interface ICreateNewUserModuleScss {
passwordField: string;
}
}

declare const CreateNewUserModuleScssModule: CreateNewUserModuleScssNamespace.ICreateNewUserModuleScss;

export = CreateNewUserModuleScssModule;
34 changes: 14 additions & 20 deletions chaoscenter/web/src/views/CreateNewUser/CreateNewUser.tsx
Original file line number Diff line number Diff line change
@@ -8,6 +8,8 @@ import * as Yup from 'yup';
import type { CreateUserMutationProps, User } from '@api/auth';
import { useStrings } from '@strings';
import { USERNAME_REGEX } from '@constants/validation';
import PasswordInput from '@components/PasswordInput';
import css from './CreateNewUser.module.scss';

interface CreateNewUserViewProps {
createNewUserMutation: UseMutateFunction<User, unknown, CreateUserMutationProps<never>, unknown>;
@@ -28,29 +30,22 @@ export default function CreateNewUserView(props: CreateNewUserViewProps): React.
const { getString } = useStrings();

function handleSubmit(values: CreateNewUserFormProps): void {
createNewUserMutation(
{
body: {
name: values.name,
email: values.email,
username: values.username,
password: values.password,
role: 'user'
}
},
{
onSuccess: () => {
handleClose();
}
createNewUserMutation({
body: {
name: values.name,
email: values.email,
username: values.username,
password: values.password,
role: 'user'
}
);
});
}

return (
<Layout.Vertical padding="medium" style={{ gap: '1rem' }}>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'space-between' }}>
<Text font={{ variation: FontVariation.H4 }}>{getString('createNewUser')}</Text>
<Icon name="cross" style={{ cursor: 'pointer' }} size={18} onClick={() => handleClose()} />
<Icon name="cross" style={{ cursor: 'pointer' }} size={18} onClick={handleClose} />
</Layout.Horizontal>
<Container>
<Formik<CreateNewUserFormProps>
@@ -99,15 +94,14 @@ export default function CreateNewUserView(props: CreateNewUserViewProps): React.
placeholder={getString('enterYourUsername')}
label={<Text font={{ variation: FontVariation.FORM_LABEL }}>{getString('username')}</Text>}
/>
<FormInput.Text
<PasswordInput
name="password"
inputGroup={{ type: 'password' }}
placeholder={getString('enterYourPassword')}
label={<Text font={{ variation: FontVariation.FORM_LABEL }}>{getString('password')}</Text>}
className={css.passwordField}
/>
<FormInput.Text
<PasswordInput
name="reEnterPassword"
inputGroup={{ type: 'password' }}
placeholder={getString('reEnterYourPassword')}
label={<Text font={{ variation: FontVariation.FORM_LABEL }}>{getString('confirmPassword')}</Text>}
/>
26 changes: 26 additions & 0 deletions mkdocs/docs/auth/v3.9.0/auth-api.json
Original file line number Diff line number Diff line change
@@ -488,6 +488,12 @@
"schema": {
"$ref": "#/definitions/User"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.ErrBadRequest"
}
}
},
"parameters": [
@@ -1837,9 +1843,16 @@
},
"response.MessageResponse": {
"type": "object",
"required": [
"message",
"projectID"
],
"properties": {
"message": {
"type": "string"
},
"projectID": {
"type": "string"
}
}
},
@@ -1868,6 +1881,19 @@
"example": "The old and new passwords can't be same"
}
}
},
"response.ErrBadRequest": {
"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"
}
}
}
}
}

0 comments on commit 7503f0d

Please sign in to comment.