diff --git a/src/Routes.jsx b/src/Routes.jsx index 7f5f48603..34466f8dd 100644 --- a/src/Routes.jsx +++ b/src/Routes.jsx @@ -91,6 +91,7 @@ const UploadDelete = React.lazy(() => import("pages/Organize/Uploads/Delete")); // Admin Pages const GroupCreate = React.lazy(() => import("pages/Admin/Group/Create")); const DeleteUser = React.lazy(() => import("pages/Admin/Users/Delete")); +const ManageGroup = React.lazy(() => import("pages/Admin/Group/Manage")); const AddLicense = React.lazy(() => import("pages/Admin/License/Create")); const SelectLicense = React.lazy(() => import("pages/Admin/License/SelectLicense") @@ -284,6 +285,11 @@ const Routes = () => { path={routes.admin.group.create} component={GroupCreate} /> + { addGroupName: false, }); }; + +// Get all group members +export const getAllGroupMembersApi = (groupId) => { + const url = endpoints.admin.groups.getAllGroupMembers(groupId); + return sendRequest({ + url, + method: "GET", + headers: { + Authorization: getToken(), + }, + }); +}; + +// Remove Group Member +export const removeGroupMemberApi = (groupId, userId) => { + const url = endpoints.admin.groups.removeGroupMember(groupId, userId); + return sendRequest({ + url, + method: "DELETE", + headers: { + Authorization: getToken(), + }, + }); +}; + +// Change user permission +export const changeUserPermissionApi = (groupId, userId, permission) => { + const url = endpoints.admin.groups.changeUserPermission(groupId, userId); + return sendRequest({ + url, + method: "PUT", + headers: { + Authorization: getToken(), + }, + body: { + perm: permission, + }, + }); +}; diff --git a/src/api/groups.test.js b/src/api/groups.test.js index 6074be0ab..cfb5f3462 100644 --- a/src/api/groups.test.js +++ b/src/api/groups.test.js @@ -15,7 +15,12 @@ import sendRequest from "api/sendRequest"; import endpoints from "constants/endpoints"; -import { createGroupApi, getAllGroupsApi } from "api/groups"; +import { + changeUserPermissionApi, + createGroupApi, + getAllGroupMembersApi, + getAllGroupsApi, removeGroupMemberApi, +} from "api/groups"; import { getToken } from "shared/authHelper"; jest.mock("api/sendRequest"); @@ -56,4 +61,63 @@ describe("groups", () => { }) ); }); + + test("removeGroupMemberApi", () => { + const groupId = 2; + const userId = 1; + const url = endpoints.admin.groups.removeGroupMember(groupId, userId); + sendRequest.mockImplementation(() => true); + + expect(removeGroupMemberApi(groupId, userId)).toBe(sendRequest({})); + expect(sendRequest).toHaveBeenCalledWith( + expect.objectContaining({ + url, + method: "DELETE", + headers: { + Authorization: getToken(), + }, + }) + ); + }); + + test("getAllGroupMembersApi", () => { + const groupId = 1; + const url = endpoints.admin.groups.getAllGroupMembers(groupId); + sendRequest.mockImplementation(() => true); + + expect(getAllGroupMembersApi(groupId)).toBe(sendRequest({})); + expect(sendRequest).toHaveBeenCalledWith( + expect.objectContaining({ + url, + method: "GET", + headers: { + Authorization: getToken(), + }, + }) + ); + }); + + test("changeUserPermissionApi", () => { + const groupId = 1; + const userId = 1; + const permission = 2; + const url = endpoints.admin.groups.changeUserPermission(groupId, userId); + sendRequest.mockImplementation(() => true); + + expect(changeUserPermissionApi(groupId, userId, permission)).toBe( + sendRequest({}) + ); + expect(sendRequest).toHaveBeenCalledWith( + expect.objectContaining({ + url, + method: "PUT", + headers: { + Authorization: getToken(), + }, + body: { + perm: permission, + }, + }) + ); + }); }); diff --git a/src/components/Admin/ChangePermission.jsx b/src/components/Admin/ChangePermission.jsx new file mode 100644 index 000000000..3f27fc121 --- /dev/null +++ b/src/components/Admin/ChangePermission.jsx @@ -0,0 +1,175 @@ +/* + Copyright (C) 2022 Samuel Dushimimana (dushsam100@gmail.com) + + SPDX-License-Identifier: GPL-2.0 + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + version 2 as published by the Free Software Foundation. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +import React, { useEffect, useState } from "react"; + +import { InputContainer } from "components/Widgets"; + +// Required functions for calling APIs +import { changeUserPermission, removeGroupMember } from "services/groups"; + +import PropTypes from "prop-types"; + +// Required constants +import { userPermissions } from "constants/constants"; + +const ChangePermissionContainer = ({ + groupMembers, + noneGroupMembers, + setShowMessage, + setMessage, + currGroup, + handleFetchGroupMembers, +}) => { + const [currUser, setCurrentUser] = useState(null); + const [currNonMember, setCurrentNonMember] = useState(null); + + useEffect(() => { + if (groupMembers.length > 0) { + setCurrentUser({ + user: groupMembers[0].id, + perm: groupMembers[0].group_perm, + }); + } + }, [groupMembers]); + + useEffect(() => { + if (noneGroupMembers.length > 0) { + setCurrentNonMember({ + user: noneGroupMembers[0].id, + perm: -1, + }); + } + }, [noneGroupMembers]); + + const handleChangeCurrUser = async (newUser, isMember = true) => { + if (isMember) { + let perm; + groupMembers.forEach((item) => { + if (item.id === parseInt(newUser, 10)) { + perm = item.group_perm; + } + }); + setCurrentUser({ user: parseInt(newUser, 10), perm }); + } else { + setCurrentNonMember({ user: parseInt(newUser, 10), perm: -1 }); + } + }; + + const handleSetNewPermission = async (newPerm, isMember = true) => { + try { + let res; + + if (parseInt(newPerm, 10) === -1) { + res = await removeGroupMember(currGroup, currUser.user); + } else { + res = await changeUserPermission( + currGroup, + isMember ? currUser.user : currNonMember.user, + parseInt(newPerm, 10) + ); + } + + setShowMessage(true); + setMessage({ + type: "success", + text: res.message, + }); + + handleFetchGroupMembers(currGroup); + } catch (e) { + setMessage({ + type: "danger", + text: e.message, + }); + } finally { + setTimeout(() => { + setShowMessage(false); + }, [3000]); + } + }; + return ( + <> + + + handleChangeCurrUser(e.target.value)} + /> + + + handleSetNewPermission(e.target.value)} + /> + + + + {noneGroupMembers.length > 0 ? ( + + + handleChangeCurrUser(e.target.value, false)} + /> + + + handleSetNewPermission(e.target.value, false)} + /> + + + ) : ( + <> + )} + + ); +}; + +ChangePermissionContainer.propTypes = { + groupMembers: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.any)).isRequired, + noneGroupMembers: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.any)) + .isRequired, + setMessage: PropTypes.func, + currGroup: PropTypes.number, + handleFetchGroupMembers: PropTypes.func, + setShowMessage: PropTypes.func, +}; + +export default ChangePermissionContainer; diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index b1c310d0a..5236a4c42 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -255,9 +255,9 @@ const Header = () => { - Delete Group + Manage Group Users diff --git a/src/components/Widgets/Input/index.jsx b/src/components/Widgets/Input/index.jsx index e1a404435..60fbd6b50 100644 --- a/src/components/Widgets/Input/index.jsx +++ b/src/components/Widgets/Input/index.jsx @@ -26,6 +26,7 @@ const InputContainer = ({ id, className, onChange, + defaultValue = null, children, checked = false, placeholder = null, @@ -70,6 +71,7 @@ const InputContainer = ({ className ? `mr-2 form-control ${className}` : `mr-2 form-control` } value={value} + defaultValue={defaultValue} onChange={onChange} multiple={multiple && multiple} size={multiple ? "15" : ""} @@ -125,6 +127,7 @@ InputContainer.propTypes = { onChange: PropTypes.func, checked: PropTypes.bool, disabled: PropTypes.bool, + defaultValue: PropTypes.string, children: PropTypes.node, options: PropTypes.arrayOf( PropTypes.shape({ diff --git a/src/constants/constants.js b/src/constants/constants.js index 857b325f9..47fb01b07 100644 --- a/src/constants/constants.js +++ b/src/constants/constants.js @@ -260,3 +260,23 @@ export const initialMantainanceFields = { rmvRepoOldFiles1: false, rmvRepoOldFiles2: false, }; + +// eslint-disable-next-line camelcase +export const userPermissions = [ + { + id: -1, + name: "None", + }, + { + id: 0, + name: "User", + }, + { + id: 1, + name: "Admin", + }, + { + id: 2, + name: "Advisor", + }, +]; diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index ef4986113..7aeb77120 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -71,6 +71,11 @@ const endpoints = { groups: { create: () => `${apiUrl}/groups`, getAll: () => `${apiUrl}/groups`, + getAllGroupMembers: (groupId) => `${apiUrl}/groups/${groupId}/members`, + changeUserPermission: (groupId, userId) => + `${apiUrl}/groups/${groupId}/user/${userId}`, + removeGroupMember: (groupId, userId) => + `${apiUrl}/groups/${groupId}/user/${userId}`, }, }, license: { diff --git a/src/constants/routes.js b/src/constants/routes.js index 358782b2b..5643df79c 100644 --- a/src/constants/routes.js +++ b/src/constants/routes.js @@ -67,6 +67,7 @@ const routes = { group: { create: "/admin/group/create", delete: "/admin/group/delete", + manageGroup: "/admin/group/manage", }, users: { delete: "/admin/users/delete", diff --git a/src/pages/Admin/Group/Manage/index.jsx b/src/pages/Admin/Group/Manage/index.jsx new file mode 100644 index 000000000..e0f69569b --- /dev/null +++ b/src/pages/Admin/Group/Manage/index.jsx @@ -0,0 +1,188 @@ +/* + Copyright (C) 2022 Samuel Dushimimana (dushsam100@gmail.com) + + SPDX-License-Identifier: GPL-2.0 + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + version 2 as published by the Free Software Foundation. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +import React, { useEffect, useState } from "react"; + +// Title +import Title from "components/Title"; + +// Widgets +import { Alert, InputContainer } from "components/Widgets"; + +// Required functions for calling APIs +import { fetchAllGroupMembers, fetchAllGroups } from "services/groups"; + +// Required page components +import ChangePermissionContainer from "components/Admin/ChangePermission"; +import { getAllUsers } from "../../../../services/users"; + +const ManageGroup = () => { + const initialMessage = { + type: "success", + text: "", + }; + + const [currGroup, setCurrentGroup] = useState(null); + const [groupMembers, setGroupMembers] = useState([]); + const [noneGroupMembers, setNoneGroupMembers] = useState([]); + + const [groups, setGroups] = useState([]); + + const [showMessage, setShowMessage] = useState(false); + const [message, setMessage] = useState(initialMessage); + + const handleSetAllUsers = async (groupId) => { + try { + const members = await fetchAllGroupMembers(groupId); + const memberUsers = []; + + for (let i = 0; i < members.length; i++) { + memberUsers.push({ + id: members[i].user.id, + group_perm: members[i].group_perm, + name: members[i].user.name, + }); + } + + setGroupMembers(memberUsers); + } catch (e) { + setMessage({ + type: "danger", + text: e.message, + }); + } + }; + + useEffect(async () => { + const users = await getAllUsers(); + + const noneMembers = []; + + for (let i = 0; i < users.length; i++) { + let isMember = false; + for (let j = 0; j < groupMembers.length; j++) { + if (users[i].id === groupMembers[j].id) { + isMember = true; + break; + } + } + if (!isMember) { + noneMembers.push(users[i]); + } + } + setNoneGroupMembers(noneMembers); + }, [groupMembers]); + + useEffect(async () => { + try { + const res = await fetchAllGroups(); + setCurrentGroup(res[0].id); + setGroups(res); + await handleSetAllUsers(res[0].id); + } catch (e) { + setMessage({ + type: "danger", + text: e.message, + }); + } + }, []); + + const handleFetchGroupMembers = async (groupId) => { + try { + await handleSetAllUsers(groupId); + } catch (error) { + setMessage({ + type: "danger", + text: error.message, + }); + } + }; + + const handleGroupChange = async (e) => { + setCurrentGroup(e.target.value); + await handleSetAllUsers(e.target.value); + }; + + return ( + <> + + <div className="main-container my-3"> + {showMessage && ( + <Alert + type={message.type} + setShow={setShowMessage} + message={message.text} + /> + )} + <h1 className="font-size-main-heading">Manage Group Users</h1> + <br /> + <div className="row"> + <div className="col-12 col-lg-8"> + <form> + <InputContainer + type="select" + name="name" + options={groups} + id="select-tag" + property="name" + onChange={(e) => handleGroupChange(e)} + value={currGroup} + > + Select group to manage: + </InputContainer> + </form> + + <table className="table table-striped table-bordered rounded mt-5"> + <thead className="bg-dark text-light font-weight-bold"> + <tr> + <th>User</th> + <th>Permission</th> + </tr> + </thead> + <tbody> + {noneGroupMembers && groupMembers && ( + <ChangePermissionContainer + currGroup={currGroup} + groupMembers={groupMembers} + noneGroupMembers={noneGroupMembers} + setShowMessage={setShowMessage} + setMessage={setMessage} + handleFetchGroupMembers={handleFetchGroupMembers} + /> + )} + </tbody> + </table> + </div> + <div className="col-10 mt-4"> + <p> + All user permissions take place immediately when a value is + changed. There is no submit button. Add new users on the last + line. + </p> + <p className="font-weight-bold"> + Note: By removing users, you may loose access to uploads, that are + uploaded by them. + </p> + </div> + </div> + </div> + </> + ); +}; + +export default ManageGroup; diff --git a/src/services/groups.js b/src/services/groups.js index ff91fd019..171e53264 100644 --- a/src/services/groups.js +++ b/src/services/groups.js @@ -16,7 +16,13 @@ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { getAllGroupsApi, createGroupApi } from "api/groups"; +import { + getAllGroupsApi, + createGroupApi, + getAllGroupMembersApi, + changeUserPermissionApi, + removeGroupMemberApi, +} from "api/groups"; import { setLocalStorage, getLocalStorage } from "shared/storageHelper"; // Fetching all the groups @@ -37,3 +43,24 @@ export const createGroup = (name) => { return res; }); }; + +// Get all group members +export const fetchAllGroupMembers = (groupId) => { + return getAllGroupMembersApi(groupId).then((res) => { + return res; + }); +}; + +// Change user permission +export const changeUserPermission = (groupId, userId, permission) => { + return changeUserPermissionApi(groupId, userId, permission).then((res) => { + return res; + }); +}; + +// Remove group member +export const removeGroupMember = (groupId, userId) => { + return removeGroupMemberApi(groupId, userId).then((res) => { + return res; + }); +};