From 5e017f58640efbaca8ace6877e5c8a846b11ce62 Mon Sep 17 00:00:00 2001 From: Vitalii Stefaniv <99541789+Vitaliistf@users.noreply.github.com> Date: Tue, 24 Sep 2024 11:06:00 +0300 Subject: [PATCH] fix: user profile not deleted without "Manage User Access" permission gf-449 (#478) * fix: added check for user self-access gf-449 * refactor: added variables for better semantic gf-449 * feat: added methods for handling user self-deletion and update gf-449 * feat: added methods for handling user self-deletion and update to the frontend gf-449 --- .../src/modules/users/user.controller.ts | 69 +++++++++++++------ .../src/modules/users/slices/actions.ts | 40 ++++++++--- .../src/modules/users/slices/users.slice.ts | 35 +++++++--- .../src/modules/users/slices/users.ts | 10 ++- apps/frontend/src/modules/users/users-api.ts | 28 ++++++++ .../delete-account/delete-account.tsx | 10 +-- .../edit-user-form/edit-user-form.tsx | 11 +-- apps/frontend/src/pages/profile/profile.tsx | 2 +- 8 files changed, 146 insertions(+), 59 deletions(-) diff --git a/apps/backend/src/modules/users/user.controller.ts b/apps/backend/src/modules/users/user.controller.ts index c80f97bc6..38f4297ee 100644 --- a/apps/backend/src/modules/users/user.controller.ts +++ b/apps/backend/src/modules/users/user.controller.ts @@ -62,15 +62,26 @@ class UserController extends BaseController { this.addRoute({ handler: (options) => - this.patch( + this.delete( options as APIHandlerOptions<{ - body: UserPatchRequestDto; params: { id: string }; }>, ), - method: "PATCH", + method: "DELETE", path: UsersApiPath.$ID, preHandlers: [checkUserPermissions([PermissionKey.MANAGE_USER_ACCESS])], + }); + + this.addRoute({ + handler: (options) => + this.patchCurrentUser( + options as APIHandlerOptions<{ + body: UserPatchRequestDto; + user: { id: string }; + }>, + ), + method: "PATCH", + path: UsersApiPath.ROOT, validation: { body: userPatchValidationSchema, }, @@ -78,14 +89,13 @@ class UserController extends BaseController { this.addRoute({ handler: (options) => - this.delete( + this.deleteCurrentUser( options as APIHandlerOptions<{ - params: { id: string }; + user: { id: string }; }>, ), method: "DELETE", - path: UsersApiPath.$ID, - preHandlers: [checkUserPermissions([PermissionKey.MANAGE_USER_ACCESS])], + path: UsersApiPath.ROOT, }); } @@ -118,6 +128,28 @@ class UserController extends BaseController { }; } + /** + * @swagger + * /users: + * delete: + * tags: + * - Users + * description: Delete the current user + * responses: + * 204: + * description: User deleted successfully + */ + private async deleteCurrentUser( + options: APIHandlerOptions<{ + user: { id: string }; + }>, + ): Promise { + return { + payload: await this.userService.delete(Number(options.user.id)), + status: HTTPCode.OK, + }; + } + /** * @swagger * /users: @@ -167,19 +199,13 @@ class UserController extends BaseController { /** * @swagger - * /users/{id}: + * /users: * patch: * tags: * - Users - * description: Update user info - * parameters: - * - in: path - * name: id - * description: ID of the user to update\ - * schema: - * type: string + * description: Update current user's information * requestBody: - * description: Updated user object\ + * description: Updated user object * content: * application/json: * schema: @@ -191,7 +217,7 @@ class UserController extends BaseController { * type: string * responses: * 200: - * description: Successful operation + * description: User updated successfully * content: * application/json: * schema: @@ -201,17 +227,16 @@ class UserController extends BaseController { * type: object * $ref: "#/components/schemas/User" */ - - private async patch( + private async patchCurrentUser( options: APIHandlerOptions<{ body: UserPatchRequestDto; - params: { id: string }; + user: { id: string }; }>, ): Promise { - const userId = Number(options.params.id); + const currentUserId = Number(options.user.id); return { - payload: await this.userService.patch(userId, options.body), + payload: await this.userService.patch(currentUserId, options.body), status: HTTPCode.OK, }; } diff --git a/apps/frontend/src/modules/users/slices/actions.ts b/apps/frontend/src/modules/users/slices/actions.ts index 6d22bc80f..2d477aea0 100644 --- a/apps/frontend/src/modules/users/slices/actions.ts +++ b/apps/frontend/src/modules/users/slices/actions.ts @@ -28,6 +28,23 @@ const deleteById = createAsyncThunk( }, ); +const deleteCurrentUser = createAsyncThunk< + boolean, + undefined, + AsyncThunkConfig +>(`${sliceName}/delete-current-user`, async (_, { dispatch, extra }) => { + const { toastNotifier, userApi } = extra; + + const isDeleted = await userApi.deleteCurrentUser(); + + if (isDeleted) { + toastNotifier.showSuccess(NotificationMessage.USER_DELETE_SUCCESS); + void dispatch(authActions.logout()); + } + + return isDeleted; +}); + const loadAll = createAsyncThunk< UserGetAllResponseDto, UserGetAllQueryParameters, @@ -38,19 +55,22 @@ const loadAll = createAsyncThunk< return userApi.getAll(query); }); -const updateProfile = createAsyncThunk< +const updateCurrentUserProfile = createAsyncThunk< UserPatchResponseDto, - { id: number; payload: UserPatchRequestDto }, + UserPatchRequestDto, AsyncThunkConfig ->(`${sliceName}/profile`, async ({ id, payload }, { dispatch, extra }) => { - const { toastNotifier, userApi } = extra; +>( + `${sliceName}/update-current-user-profile`, + async (payload, { dispatch, extra }) => { + const { toastNotifier, userApi } = extra; - const user = await userApi.patch(id, payload); - void dispatch(authActions.getAuthenticatedUser()); + const user = await userApi.patchCurrentUser(payload); + void dispatch(authActions.getAuthenticatedUser()); - toastNotifier.showSuccess(NotificationMessage.PROFILE_UPDATE_SUCCESS); + toastNotifier.showSuccess(NotificationMessage.PROFILE_UPDATE_SUCCESS); - return user; -}); + return user; + }, +); -export { deleteById, loadAll, updateProfile }; +export { deleteById, deleteCurrentUser, loadAll, updateCurrentUserProfile }; diff --git a/apps/frontend/src/modules/users/slices/users.slice.ts b/apps/frontend/src/modules/users/slices/users.slice.ts index c9aa83d10..1f2e435cd 100644 --- a/apps/frontend/src/modules/users/slices/users.slice.ts +++ b/apps/frontend/src/modules/users/slices/users.slice.ts @@ -5,7 +5,12 @@ import { DataStatus } from "~/libs/enums/enums.js"; import { type ValueOf } from "~/libs/types/types.js"; import { type UserGetAllItemResponseDto } from "~/modules/users/users.js"; -import { deleteById, loadAll, updateProfile } from "./actions.js"; +import { + deleteById, + deleteCurrentUser, + loadAll, + updateCurrentUserProfile, +} from "./actions.js"; type State = { dataStatus: ValueOf; @@ -39,25 +44,37 @@ const { actions, name, reducer } = createSlice({ state.dataStatus = DataStatus.REJECTED; }); - builder.addCase(updateProfile.pending, (state) => { + builder.addCase(deleteById.pending, (state) => { + state.deleteStatus = DataStatus.PENDING; + }); + builder.addCase(deleteById.fulfilled, (state, action) => { + const { id } = action.meta.arg; + state.users = state.users.filter((user) => user.id !== id); + state.usersTotalCount -= ITEMS_CHANGED_COUNT; + state.deleteStatus = DataStatus.FULFILLED; + }); + builder.addCase(deleteById.rejected, (state) => { + state.deleteStatus = DataStatus.REJECTED; + }); + + builder.addCase(updateCurrentUserProfile.pending, (state) => { state.updateProfileStatus = DataStatus.PENDING; }); - builder.addCase(updateProfile.fulfilled, (state) => { + builder.addCase(updateCurrentUserProfile.fulfilled, (state) => { state.updateProfileStatus = DataStatus.FULFILLED; }); - builder.addCase(updateProfile.rejected, (state) => { + builder.addCase(updateCurrentUserProfile.rejected, (state) => { state.updateProfileStatus = DataStatus.REJECTED; }); - builder.addCase(deleteById.pending, (state) => { + + builder.addCase(deleteCurrentUser.pending, (state) => { state.deleteStatus = DataStatus.PENDING; }); - builder.addCase(deleteById.fulfilled, (state, action) => { - const { id } = action.meta.arg; - state.users = state.users.filter((user) => user.id !== id); + builder.addCase(deleteCurrentUser.fulfilled, (state) => { state.usersTotalCount -= ITEMS_CHANGED_COUNT; state.deleteStatus = DataStatus.FULFILLED; }); - builder.addCase(deleteById.rejected, (state) => { + builder.addCase(deleteCurrentUser.rejected, (state) => { state.deleteStatus = DataStatus.REJECTED; }); }, diff --git a/apps/frontend/src/modules/users/slices/users.ts b/apps/frontend/src/modules/users/slices/users.ts index 1b540ba91..e73c5058b 100644 --- a/apps/frontend/src/modules/users/slices/users.ts +++ b/apps/frontend/src/modules/users/slices/users.ts @@ -1,11 +1,17 @@ -import { deleteById, loadAll, updateProfile } from "./actions.js"; +import { + deleteById, + deleteCurrentUser, + loadAll, + updateCurrentUserProfile, +} from "./actions.js"; import { actions } from "./users.slice.js"; const allActions = { ...actions, deleteById, + deleteCurrentUser, loadAll, - updateProfile, + updateCurrentUserProfile, }; export { allActions as actions }; diff --git a/apps/frontend/src/modules/users/users-api.ts b/apps/frontend/src/modules/users/users-api.ts index 75857a8c0..1cda8eaf3 100644 --- a/apps/frontend/src/modules/users/users-api.ts +++ b/apps/frontend/src/modules/users/users-api.ts @@ -34,6 +34,18 @@ class UserApi extends BaseHTTPApi { return await response.json(); } + public async deleteCurrentUser(): Promise { + const response = await this.load( + this.getFullEndpoint(UsersApiPath.ROOT, {}), + { + hasAuth: true, + method: "DELETE", + }, + ); + + return await response.json(); + } + public async getAll({ name = "", page, @@ -71,6 +83,22 @@ class UserApi extends BaseHTTPApi { return await response.json(); } + + public async patchCurrentUser( + payload: UserPatchRequestDto, + ): Promise { + const response = await this.load( + this.getFullEndpoint(UsersApiPath.ROOT, {}), + { + contentType: ContentType.JSON, + hasAuth: true, + method: "PATCH", + payload: JSON.stringify(payload), + }, + ); + + return await response.json(); + } } export { UserApi }; diff --git a/apps/frontend/src/pages/profile/libs/components/delete-account/delete-account.tsx b/apps/frontend/src/pages/profile/libs/components/delete-account/delete-account.tsx index a4bff64e7..0a4ee86a6 100644 --- a/apps/frontend/src/pages/profile/libs/components/delete-account/delete-account.tsx +++ b/apps/frontend/src/pages/profile/libs/components/delete-account/delete-account.tsx @@ -5,11 +5,7 @@ import { actions as userActions } from "~/modules/users/users.js"; import styles from "./styles.module.css"; -type Properties = { - userId: number; -}; - -const DeleteAccount = ({ userId }: Properties): JSX.Element => { +const DeleteAccount = (): JSX.Element => { const dispatch = useAppDispatch(); const { isOpened, onClose, onOpen } = useModal(); @@ -19,9 +15,9 @@ const DeleteAccount = ({ userId }: Properties): JSX.Element => { }, [onOpen]); const handleDeleteConfirm = useCallback(() => { - void dispatch(userActions.deleteById({ id: userId })); + void dispatch(userActions.deleteCurrentUser()); void dispatch(authActions.logout()); - }, [dispatch, userId]); + }, [dispatch]); return (
diff --git a/apps/frontend/src/pages/profile/libs/components/edit-user-form/edit-user-form.tsx b/apps/frontend/src/pages/profile/libs/components/edit-user-form/edit-user-form.tsx index 60294eede..759350c1e 100644 --- a/apps/frontend/src/pages/profile/libs/components/edit-user-form/edit-user-form.tsx +++ b/apps/frontend/src/pages/profile/libs/components/edit-user-form/edit-user-form.tsx @@ -14,7 +14,7 @@ type Properties = { }; const EditUserForm = ({ user }: Properties): JSX.Element => { - const { email, id: userId, name } = user; + const { email, name } = user; const dispatch = useAppDispatch(); const { control, errors, handleSubmit, isDirty } = @@ -29,15 +29,10 @@ const EditUserForm = ({ user }: Properties): JSX.Element => { const handleFormSubmit = useCallback( (event_: React.BaseSyntheticEvent): void => { void handleSubmit((formData: UserPatchRequestDto) => { - void dispatch( - usersActions.updateProfile({ - id: userId, - payload: formData, - }), - ); + void dispatch(usersActions.updateCurrentUserProfile(formData)); })(event_); }, - [dispatch, handleSubmit, userId], + [dispatch, handleSubmit], ); return ( diff --git a/apps/frontend/src/pages/profile/profile.tsx b/apps/frontend/src/pages/profile/profile.tsx index 5a41fa09a..ace0cf3ef 100644 --- a/apps/frontend/src/pages/profile/profile.tsx +++ b/apps/frontend/src/pages/profile/profile.tsx @@ -17,7 +17,7 @@ const Profile = (): JSX.Element => {
- +
);