diff --git a/src/Routes.jsx b/src/Routes.jsx index 2705f4105..e838c1997 100644 --- a/src/Routes.jsx +++ b/src/Routes.jsx @@ -92,6 +92,7 @@ const UploadDelete = React.lazy(() => import("pages/Organize/Uploads/Delete")); const GroupCreate = React.lazy(() => import("pages/Admin/Group/Create")); const DeleteGroup = React.lazy(() => import("pages/Admin/Group/Delete")); const DeleteUser = React.lazy(() => import("pages/Admin/Users/Delete")); +const ManageGroup = React.lazy(() => import("pages/Admin/Group/Manage")); const AddUser = React.lazy(() => import("pages/Admin/Users/Add")); const EditUser = React.lazy(() => import("pages/Admin/Users/Edit")); const AddLicense = React.lazy(() => import("pages/Admin/License/Create")); @@ -287,6 +288,11 @@ const Routes = () => { path={routes.admin.group.create} component={GroupCreate} /> + { }); }; +// 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(), + }, + }); +}; // Delete a group export const deleteGroupApi = (id) => { const url = endpoints.admin.groups.delete(id); @@ -73,6 +96,21 @@ export const deleteGroupApi = (id) => { 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, + }, addGroupName: false, }); }; diff --git a/src/api/groups.test.js b/src/api/groups.test.js index 6074be0ab..f056db65c 100644 --- a/src/api/groups.test.js +++ b/src/api/groups.test.js @@ -15,7 +15,13 @@ 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 +62,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..2fac252c9 --- /dev/null +++ b/src/components/Admin/ChangePermission.jsx @@ -0,0 +1,103 @@ +/* + 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 = ({ + perm, + setShowMessage, + setMessage, + currGroup, + user, + handleFetchGroupMembers, +}) => { + const [selectedPerm, setSelectedPerm] = useState(perm); + + useEffect(() => { + setSelectedPerm(perm); + }, [perm]); + + const handleSetNewPermission = async (newPerm) => { + setSelectedPerm(newPerm); + // eslint-disable-next-line no-console + try { + let res; + if (parseInt(newPerm, 10) === -1) { + // eslint-disable-next-line no-console + console.log(`HERE ID ${newPerm}`); + res = await removeGroupMember(currGroup, user.id); + } else { + res = await changeUserPermission(currGroup, user.id, newPerm); + } + + setShowMessage(true); + setMessage({ + type: "success", + text: res.message, + }); + + handleFetchGroupMembers(currGroup); + } catch (e) { + setMessage({ + type: "danger", + text: e.message, + }); + } finally { + setTimeout(() => { + setShowMessage(false); + }, [3000]); + } + }; + return ( + + {user.name} + + handleSetNewPermission(e.target.value)} + /> + + + ); +}; + +ChangePermissionContainer.propTypes = { + perm: PropTypes.number, + setMessage: PropTypes.func, + currGroup: PropTypes.number, + user: PropTypes.node, + handleFetchGroupMembers: PropTypes.func, + setShowMessage: PropTypes.func, +}; + +export default ChangePermissionContainer; diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index bc6b0f47c..4d42c3eb7 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -258,9 +258,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 5a9722626..50e421463 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 dc4cd7f2b..29e680c74 100644 --- a/src/constants/constants.js +++ b/src/constants/constants.js @@ -356,3 +356,23 @@ export const agents = { reso: "REUSE.Software Analysis (forces *Ojo License Analysis*)", heritage: "Software Heritage Analysis", }; + +// 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 634ddc553..5ff3fbacb 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -78,6 +78,11 @@ const endpoints = { getAll: () => `${apiUrl}/groups`, getAllDeletable: () => `${apiUrl}/groups/deletable`, delete: (groupId) => `${apiUrl}/groups/${groupId}`, + 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 832858e97..59fe2587d 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: { add: "/admin/users/add", diff --git a/src/pages/Admin/Group/Manage/index.jsx b/src/pages/Admin/Group/Manage/index.jsx new file mode 100644 index 000000000..226c28df4 --- /dev/null +++ b/src/pages/Admin/Group/Manage/index.jsx @@ -0,0 +1,179 @@ +/* + 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 [allUsers, setAllUsers] = useState([]); + // const [groupMembers, setGroupMembers] = 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 users = await getAllUsers(); + // setGroupMembers(members); + for (let i = 0; i < users.length; i++) { + users[i].isMember = false; + users[i].group_perm = -1; + for (let j = 0; j < members.length; j++) { + if (users[i].id === members[j].user.id) { + users[i].isMember = true; + users[i].group_perm = members[j].group_perm; + break; + } + } + } + + // eslint-disable-next-line no-console + console.log("altered users ", users); + // eslint-disable-next-line no-console + console.log("altered members ", members); + setAllUsers(users); + } catch (e) { + setMessage({ + type: "danger", + text: e.message, + }); + } + }; + + 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 { + // const res = await fetchAllGroupMembers(groupId); + await handleSetAllUsers(groupId); + // setGroupMembers(res); + } catch (error) { + setMessage({ + type: "danger", + text: error.message, + }); + } + }; + + const handleGroupChange = async (e) => { + setCurrentGroup(e.target.value); + await handleSetAllUsers(e.target.value); + // await handleFetchGroupMembers(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> + {allUsers?.map((user) => ( + <ChangePermissionContainer + currGroup={currGroup} + user={user} + handleFetchGroupMembers={handleFetchGroupMembers} + perm={user.group_perm} + setShowMessage={setShowMessage} + setMessage={setMessage} + key={user.id} + /> + ))} + </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 d44995940..8ea5758a9 100644 --- a/src/services/groups.js +++ b/src/services/groups.js @@ -20,6 +20,9 @@ import { getAllGroupsApi, createGroupApi, + getAllGroupMembersApi, + changeUserPermissionApi, + removeGroupMemberApi, deleteGroupApi, getAllDeletableGroupsApi, } from "api/groups"; @@ -44,6 +47,12 @@ export const createGroup = (name) => { }); }; +// Get all group members +export const fetchAllGroupMembers = (groupId) => { + return getAllGroupMembersApi(groupId).then((res) => { + return res; + }); +}; // Delete a group export const deleteGroup = (id) => { return deleteGroupApi(id).then((res) => { @@ -51,6 +60,19 @@ export const deleteGroup = (id) => { }); }; +// 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; + }); +}; // Fetching all deletable groups export const fetchAllDeletableGroups = () => { return getAllDeletableGroupsApi().then((res) => {