Skip to content

Commit

Permalink
fix: user profile not deleted without "Manage User Access" permission…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
Vitaliistf authored Sep 24, 2024
1 parent 1479786 commit 5e017f5
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 59 deletions.
69 changes: 47 additions & 22 deletions apps/backend/src/modules/users/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,30 +62,40 @@ 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,
},
});

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,
});
}

Expand Down Expand Up @@ -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<APIHandlerResponse> {
return {
payload: await this.userService.delete(Number(options.user.id)),
status: HTTPCode.OK,
};
}

/**
* @swagger
* /users:
Expand Down Expand Up @@ -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:
Expand All @@ -191,7 +217,7 @@ class UserController extends BaseController {
* type: string
* responses:
* 200:
* description: Successful operation
* description: User updated successfully
* content:
* application/json:
* schema:
Expand All @@ -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<APIHandlerResponse> {
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,
};
}
Expand Down
40 changes: 30 additions & 10 deletions apps/frontend/src/modules/users/slices/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@ const deleteById = createAsyncThunk<boolean, { id: number }, AsyncThunkConfig>(
},
);

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,
Expand All @@ -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 };
35 changes: 26 additions & 9 deletions apps/frontend/src/modules/users/slices/users.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof DataStatus>;
Expand Down Expand Up @@ -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;
});
},
Expand Down
10 changes: 8 additions & 2 deletions apps/frontend/src/modules/users/slices/users.ts
Original file line number Diff line number Diff line change
@@ -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 };
Expand Down
28 changes: 28 additions & 0 deletions apps/frontend/src/modules/users/users-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ class UserApi extends BaseHTTPApi {
return await response.json<boolean>();
}

public async deleteCurrentUser(): Promise<boolean> {
const response = await this.load(
this.getFullEndpoint(UsersApiPath.ROOT, {}),
{
hasAuth: true,
method: "DELETE",
},
);

return await response.json<boolean>();
}

public async getAll({
name = "",
page,
Expand Down Expand Up @@ -71,6 +83,22 @@ class UserApi extends BaseHTTPApi {

return await response.json<UserPatchResponseDto>();
}

public async patchCurrentUser(
payload: UserPatchRequestDto,
): Promise<UserPatchResponseDto> {
const response = await this.load(
this.getFullEndpoint(UsersApiPath.ROOT, {}),
{
contentType: ContentType.JSON,
hasAuth: true,
method: "PATCH",
payload: JSON.stringify(payload),
},
);

return await response.json<UserPatchResponseDto>();
}
}

export { UserApi };
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 (
<div className={styles["profile-delete"]}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 } =
Expand All @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/pages/profile/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const Profile = (): JSX.Element => {
<EditUserForm user={user} />
</div>
<div className={styles["divider"]} />
<DeleteAccount userId={user.id} />
<DeleteAccount />
</div>
</PageLayout>
);
Expand Down

0 comments on commit 5e017f5

Please sign in to comment.